/**
 * StitchPress — customer capture modal.
 *
 * Brand-neutral by design. Every color, radius and spacing value is a custom
 * property with a sane default, so a shop or theme overrides the look by
 * redefining a handful of variables rather than fighting selectors:
 *
 *   .stp-capture { --stp-accent: #c8102e; --stp-radius: 2px; }
 *
 * Everything is namespaced under .stp-capture (and .stp-capture-launcher on the
 * product page) — no bare element selectors escape into the theme. Mobile
 * first: the single-column stack is the base, the two-column layout is the
 * enhancement. Motion is opt-out via prefers-reduced-motion.
 */

.stp-capture,
.stp-capture-launcher,
.stp-cart-edit {
	--stp-accent: #1f5eff;
	--stp-accent-contrast: #ffffff;
	--stp-ink: #1a1a1a;
	--stp-ink-muted: #5b6470;
	--stp-surface: #ffffff;
	--stp-surface-alt: #f5f6f8;
	--stp-border: #d8dce3;
	--stp-border-strong: #858e9b;
	--stp-danger: #b3261e;
	--stp-success: #1a7f4b;
	--stp-notice: #7a5200;
	/*
	 * ADVISORY AMBER — the warning strip and the "Check" bullets in the facts
	 * sheet, and NEVER --stp-danger.
	 *
	 * The distinction is a product rule, not a palette preference: nothing the
	 * estimator says can stop Add to cart. "Your text works out about 0.19"
	 * tall" is something the shop will talk to the customer about before it
	 * runs the job, and painting it the same red as "your customization could
	 * not be saved" tells a customer the order is broken when it is not.
	 * Derived from --stp-notice, which is already this file's amber ink, so a
	 * shop that recolors one gets all three.
	 */
	--stp-notice-bg: rgba( 122, 82, 0, 0.08 );
	--stp-notice-edge: rgba( 122, 82, 0, 0.45 );
	--stp-radius: 6px;
	--stp-radius-sm: 4px;
	--stp-radius-lg: 12px;
	/*
	 * The inline card's own corner. Larger than --stp-radius-lg on purpose: the
	 * card is a product surface sitting in the page's own flow, not a panel
	 * inside a dialog, and it is the one radius a shop is most likely to want to
	 * match against its theme's cards.
	 */
	--stp-radius-card: 14px;
	--stp-gap: 16px;
	--stp-gap-sm: 8px;
	--stp-shadow: 0 12px 40px rgba( 15, 23, 42, 0.24 );
	--stp-focus: 0 0 0 3px rgba( 31, 94, 255, 0.35 );
	--stp-swatch-size: 34px;
	--stp-zone-outline: rgba( 255, 255, 255, 0.9 );
	/*
	 * Visual placement. The halo is what makes the selection frame and the
	 * handles readable on a white tee AND on a black cap — the item sits on a
	 * photograph whose colors we do not control, so every stage affordance is
	 * drawn twice: accent on top of a light halo.
	 */
	--stp-halo: rgba( 255, 255, 255, 0.92 );
	--stp-handle-hit: 44px;
	--stp-handle-dot: 14px;
	/*
	 * Studio layout (visual mode only). A neutral working surface behind the
	 * garment, the height the stage is given, and the size of the panel that
	 * comes up over it — a sheet on a phone, a drawer beside the canvas from
	 * 720px. All four are properties like everything else here, so a shop that
	 * wants a taller stage or a darker studio changes a number, not a selector.
	 */
	/*
	 * WHITE. The canvas is a product photograph on a shop's product page, and
	 * every retail designer worth copying puts it on white — a tinted working
	 * surface is the thing that makes a designer read as an application. Kept a
	 * token because a shop selling white garments may well want the tint back.
	 */
	--stp-canvas-bg: #ffffff;
	--stp-stage-h: 55vh;
	--stp-drawer-w: 320px;
	/*
	 * The card's CHROME — the top bar and the tool bar that frame the garment.
	 *
	 * Deliberately NOT --stp-accent and deliberately not derived from it: the
	 * chrome is a neutral frame whose whole job is to disappear around the
	 * product photograph, while the accent is what marks the customer's own
	 * choices inside it (a selected swatch, a filled area, the item being
	 * dragged). A shop that recolors one almost never means the other, and the
	 * two are the pair most likely to be set to the same value by accident.
	 *
	 * Two tokens rather than a palette: a background and the ink on it. Every
	 * state on the bars — hover, pressed, disabled — is a white overlay at some
	 * opacity over the background, so one value moves the whole surface and
	 * nothing has to be recomputed to stay legible against it.
	 */
	--stp-chrome-bg: #1b2c46;
	--stp-chrome-ink: #ffffff;
	/*
	 * ACCENT AS TEXT. --stp-accent is a fill colour; a skin whose accent is a
	 * light gold cannot set text in it and pass AA. Every text site that wants
	 * the accent reads this instead, and it defaults to the accent itself, so the
	 * neutral look is unchanged and a skin sets a darker ink beside its fill.
	 */
	--stp-accent-ink: var( --stp-accent );
	/*
	 * Fonts by NAME only — the plugin ships none. `inherit` keeps the theme's
	 * type until a skin names a family the theme already loads.
	 */
	--stp-font-ui: inherit;
	--stp-font-display: inherit;
	/*
	 * TINTS AND SHADOWS, one token per literal so no colour is hard-coded
	 * outside this block. Named by family and opacity (light = a white veil,
	 * shade = the slate shadow, dark = a neutral shadow); the defaults are the
	 * exact values the rules used before, so the neutral look is unchanged.
	 */
	--stp-accent-tint-06: rgba( 31, 94, 255, 0.06 );
	--stp-accent-tint-14: rgba( 31, 94, 255, 0.14 );
	--stp-chrome-fade: rgba( 27, 44, 70, 0 );
	--stp-danger-08: rgba( 179, 38, 30, 0.08 );
	--stp-danger-25: rgba( 179, 38, 30, 0.25 );
	--stp-dark-25: rgba( 0, 0, 0, 0.25 );
	--stp-dark-80: rgba( 0, 0, 0, 0.8 );
	--stp-ink-placeholder: #667085;
	--stp-light-08: rgba( 255, 255, 255, 0.08 );
	--stp-light-10: rgba( 255, 255, 255, 0.1 );
	--stp-light-12: rgba( 255, 255, 255, 0.12 );
	--stp-light-14: rgba( 255, 255, 255, 0.14 );
	--stp-light-16: rgba( 255, 255, 255, 0.16 );
	--stp-light-18: rgba( 255, 255, 255, 0.18 );
	--stp-light-22: rgba( 255, 255, 255, 0.22 );
	--stp-light-28: rgba( 255, 255, 255, 0.28 );
	--stp-light-34: rgba( 255, 255, 255, 0.34 );
	--stp-light-42: rgba( 255, 255, 255, 0.42 );
	--stp-light-50: rgba( 255, 255, 255, 0.5 );
	--stp-light-60: rgba( 255, 255, 255, 0.6 );
	--stp-light-85: rgba( 255, 255, 255, 0.85 );
	--stp-light-86: rgba( 255, 255, 255, 0.86 );
	--stp-light-95: rgba( 255, 255, 255, 0.95 );
	--stp-notice-08: rgba( 122, 82, 0, 0.08 );
	--stp-notice-14: rgba( 122, 82, 0, 0.14 );
	--stp-notice-25: rgba( 122, 82, 0, 0.25 );
	--stp-shade-06: rgba( 15, 23, 42, 0.06 );
	--stp-shade-08: rgba( 15, 23, 42, 0.08 );
	--stp-shade-14: rgba( 15, 23, 42, 0.14 );
	--stp-shade-16: rgba( 15, 23, 42, 0.16 );
	--stp-shade-18: rgba( 15, 23, 42, 0.18 );
	--stp-shade-55: rgba( 15, 23, 42, 0.55 );
	--stp-success-08: rgba( 26, 127, 75, 0.08 );
	--stp-success-30: rgba( 26, 127, 75, 0.3 );
	font-family: var( --stp-font-ui );
}

/*
 * BORDER-BOX FOR THE WHOLE CARD, stated rather than inherited.
 *
 * `width: 100%` on a content-box control is 100% PLUS its padding and border,
 * which overflows whatever holds it. Most themes set border-box globally and
 * hid this; Twenty Twenty-Five does not, and on it the sheet's text field ran
 * ~10px past its right edge — found the moment the sheet got big enough to
 * show a field and a swatch grid together (0.9.0).
 *
 * 0.9.0 fixed it on the three form-control classes. That was under-scoped: the
 * same bug survived on `.stp-capture__submit` (width:100% + 40px of padding —
 * Add to cart, four times the overflow of the field that was fixed), on the
 * editor toggle, on every 44px swatch in an auto-fill grid, and on the inline
 * dialog itself (9th review round, MAJOR 4). One rule instead, because the
 * three-class version needs re-doing every time a control is added.
 *
 * SCOPE: every selector is anchored to `.stp-capture`, so this cannot reach a
 * theme element — the card's own subtree and nothing else. It is deliberately
 * NOT applied to `.stp-capture-launcher` or `.stp-cart-edit`, which are small
 * fragments rendered inside the theme's own markup.
 */
.stp-capture,
.stp-capture *,
.stp-capture *::before,
.stp-capture *::after {
	box-sizing: border-box;
}

/* -------------------------------------------------------------------------
 * Launcher (in form.cart)
 * ---------------------------------------------------------------------- */

.stp-capture-launcher {
	margin: 0 0 var( --stp-gap );
}

.stp-capture-launcher__summary {
	margin: var( --stp-gap-sm ) 0 0;
	color: var( --stp-ink-muted );
	font-size: 0.875em;
}

.stp-capture-launcher__summary:empty {
	display: none;
}

/*
 * Post-add confirmation. It replaces a page reload as the customer's only
 * evidence that anything happened, so it does not fade out on a timer and it
 * carries the obvious next step with it.
 */
.stp-capture-launcher__flash {
	margin: var( --stp-gap-sm ) 0 0;
	padding: var( --stp-gap-sm ) 12px;
	color: var( --stp-success );
	background: var( --stp-success-08 );
	border: 1px solid var( --stp-success-30 );
	border-radius: var( --stp-radius-sm );
	font-size: 0.875em;
}

.stp-capture-launcher__flash[hidden] {
	display: none;
}

.stp-capture-launcher__flash-link {
	color: inherit;
	text-decoration: underline;
}

/* -------------------------------------------------------------------------
 * Cart re-edit link (rendered into the cart's product-name cell)
 * ---------------------------------------------------------------------- */

.stp-cart-edit {
	display: inline-block;
	margin-top: 4px;
	font-size: 0.8125em;
	color: var( --stp-accent-ink );
	text-decoration: underline;
}

/* -------------------------------------------------------------------------
 * Overlay + dialog
 * ---------------------------------------------------------------------- */

body.stp-capture-open {
	overflow: hidden;
}

.stp-capture__overlay {
	position: fixed;
	inset: 0;
	z-index: 100000;
	display: flex;
	align-items: flex-end;
	justify-content: center;
	padding: 0;
	background: var( --stp-shade-55 );
	overflow-y: auto;
}

.stp-capture__overlay[hidden] {
	display: none;
}

.stp-capture__dialog {
	position: relative;
	display: flex;
	flex-direction: column;
	width: 100%;
	max-width: 960px;
	max-height: 100%;
	overflow: hidden;
	background: var( --stp-surface );
	border-radius: var( --stp-radius ) var( --stp-radius ) 0 0;
	box-shadow: var( --stp-shadow );
	color: var( --stp-ink );
	font-size: 16px;
	line-height: 1.45;
}

.stp-capture__dialog:focus {
	outline: none;
}

/* -------------------------------------------------------------------------
 * Inline card
 *
 * The same dialog, re-parented into a container the server printed in the
 * product page's own flow (STP_Capture::display_mode). Everything below is
 * about it no longer being an overlay: no viewport cap, no bottom-sheet
 * corners, no close button — there is nothing to close.
 * ---------------------------------------------------------------------- */

.stp-capture-embed {
	display: block;
	width: 100%;
	margin: 0 0 var( --stp-gap );
}

/*
 * ONE ADD-TO-CART. The inline card sits inside form.cart with the theme's own
 * submit button rendered right under it — two same-named primary actions, of
 * which only the card's runs preflight(), the estimate and the spec hand-off.
 * capture.js stamps this class on the form at the moment the card mounts
 * (mountInline), so the theme's button exists exactly when the card does not:
 * no JS, no class, native button, full fallback. display:none is safe for the
 * card's own escape hatch — requestSubmit() accepts a hidden submitter and
 * form.submit() never needed a visible one — and it removes the button from
 * the tab order along with the paint, which a clip-hide would not.
 *
 * The quantity field is left alone: the serialized design is per-unit and the
 * customer's quantity rides the same form either path.
 *
 * `!important`, deliberately — the competition here is other people's CSS.
 * Woo's own woocommerce-blocktheme.css re-asserts the button at
 * `.woocommerce div.product form.cart button.single_add_to_cart_button`
 * (0,4,3), and every theme is free to go higher. This rule exists to switch
 * a control OFF while the card owns the action; it is the same class of
 * deliberate override as the reduced-motion `animation: none !important`
 * below, and out-specifying an open set of themes is a losing game.
 *
 * `visibility`, NOT `display`. Block themes size the product summary column
 * to its content, and the qty+button row is routinely that column's widest
 * item — `display: none` shrank the whole column (and the card in it) from
 * 315px to 239px on Twenty Twenty-Five at 375vw, measured live. visibility
 * keeps the button's box propping the column while removing paint, taps,
 * tab order and the accessibility node; the fallback submit path is
 * indifferent either way (requestSubmit() accepts an invisible submitter).
 */
form.cart.stp-capture-owns-submit .single_add_to_cart_button,
form.cart.stp-capture-owns-submit button[type="submit"][name="add-to-cart"],
form.cart.stp-capture-owns-submit input[type="submit"][name="add-to-cart"] {
	visibility: hidden !important;
}

/*
 * The theme's quantity row goes invisible WITH its button (8th-round finding:
 * hiding only the button left a bare unlabeled "1" spinner floating beside an
 * invisible hole). Same visibility technique, same reason — the row's box
 * keeps propping content-sized block-theme columns. The customer still sets
 * quantity: the card's footer grows its own stepper (mountInline), two-way
 * synced with this hidden input, which remains the value the form actually
 * posts.
 */
form.cart.stp-capture-owns-submit .quantity {
	visibility: hidden !important;
}

.stp-capture__dialog.is-inline {
	/*
	 * THE DESIGNER FILLS THE SCREEN; the standing text is met by scrolling.
	 *
	 * Real-device pass 2026-08-20 (Aramis): "make the designer the full screen
	 * and scroll for the footer with the disclaimer and stuff". So the garment
	 * is sized to what is left of the viewport after the card's own fixed
	 * chrome — top bar, mode pills, tool rail, action bar, borders — which puts
	 * Add to cart at the bottom of the screen and every paragraph below the
	 * fold, where standing text belongs.
	 *
	 * THE CHROME IS MEASURED, NOT GUESSED. 0.9.0 wrote "call it 260px" here and
	 * was wrong by ~180: the real total at 430x932 is ~440 (top bar 52, mode
	 * pills 38, rail 66, action-bar footer ~282, borders). The error was
	 * invisible with a 6:7 garment, which is WIDTH-limited on a phone and never
	 * reaches the cap at all; any view image taller than about 3:4 — a hoodie, a
	 * full-length apron, a 600x1200 product shot — took the full 672px and put
	 * the primary button ~200px BELOW the fold, which is the exact defect the
	 * declaration was written to fix (9th review round, MAJOR 2). capture.js
	 * measures the real parts and writes --stp-chrome-h in pixels on this
	 * element, on build and again on every resize (applyChromeHeight).
	 *
	 * The 260px here is the PRE-JS FRAME only — the value that holds for the
	 * one layout that happens before the measurement lands. It is deliberately
	 * the old constant: too small rather than too large, so the first frame
	 * shows a garment that is slightly too big rather than a strip.
	 *
	 * `svh` is the SMALL viewport height: on a phone that is the height with
	 * the browser's toolbars SHOWN, which is the state the customer is in when
	 * the page loads. Plain `vh` there is the toolbar-hidden height, and sizing
	 * to it would push the button under the toolbar — the one thing that must
	 * stay on screen. The `vh` line above it is the fallback for browsers that
	 * do not know `svh` (they drop the second declaration and keep the first),
	 * and the floor stops a short viewport from squeezing the garment to a
	 * strip. BOTH lines carry the floor: without it, a viewport shorter than
	 * the chrome makes the calc negative, `max-height` rejects a negative
	 * length, the declaration is invalid at computed-value time and the cap
	 * falls back to `none` — the garment then renders at its natural size,
	 * which is the opposite of what a short viewport needs. Inline only: the
	 * modal is capped to the viewport by its own rules and keeps the 55vh stage
	 * it was tuned with.
	 *
	 * THE FLOOR IS A LAST RESORT, NOT A LAYOUT. Once it engages, the garment
	 * has stopped shrinking and the chrome under it is what overflows instead —
	 * so the short-viewport block further down takes the stage off this
	 * expression entirely from the height where that begins. That height is
	 * derived from these two numbers; the arithmetic is written out beside the
	 * media query, and the two move together.
	 */
	--stp-chrome-h: 260px;
	--stp-stage-h: max( 260px, calc( 100vh - var( --stp-chrome-h ) ) );
	--stp-stage-h: max( 260px, calc( 100svh - var( --stp-chrome-h ) ) );
	max-width: none;
	/*
	 * NOT capped to the viewport. The card scrolls with the page like anything
	 * else on it; the stage inside keeps its own --stp-stage-h cap, which is what
	 * stops a tall garment from filling a phone screen on its own.
	 */
	max-height: none;
	border: 1px solid var( --stp-border );
	border-radius: var( --stp-radius-card );
	box-shadow:
		0 1px 2px var( --stp-shade-06 ),
		0 10px 30px var( --stp-shade-08 );
}

/*
 * The header is the dialog's title and its close button. Inline, the first is
 * said by the card's own aria-label and the second closes nothing.
 */
.stp-capture__dialog.is-inline > .stp-capture__header {
	display: none;
}

.stp-capture__dialog :focus-visible {
	outline: 2px solid var( --stp-accent );
	outline-offset: 2px;
	box-shadow: var( --stp-focus );
}

/* -------------------------------------------------------------------------
 * Header
 * ---------------------------------------------------------------------- */

.stp-capture__header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var( --stp-gap-sm );
	padding: var( --stp-gap );
	border-bottom: 1px solid var( --stp-border );
}

.stp-capture__title {
	margin: 0;
	font-size: 1.125em;
	line-height: 1.2;
}

.stp-capture__close {
	flex: 0 0 auto;
	width: 40px;
	height: 40px;
	padding: 0;
	font-size: 24px;
	line-height: 1;
	color: var( --stp-ink-muted );
	background: transparent;
	border: 1px solid transparent;
	border-radius: var( --stp-radius-sm );
	cursor: pointer;
}

.stp-capture__close:hover {
	color: var( --stp-ink );
	border-color: var( --stp-border );
}

/* -------------------------------------------------------------------------
 * Card chrome: the dark top bar (visual mode only)
 *
 * Preview on the left, undo/redo in the middle, Info on the right. The bar
 * spans the whole card at every width — unlike the tool bar below it, which
 * stands up into a left column from 720px — because what it carries is about
 * the card as a whole rather than about the garment in it.
 * ---------------------------------------------------------------------- */

.stp-capture__topbar {
	display: flex;
	align-items: center;
	gap: var( --stp-gap-sm );
	padding: 4px 8px;
	color: var( --stp-chrome-ink );
	background: var( --stp-chrome-bg );
}

/*
 * THE HISTORY GROUP IS CENTRED, and space-between could not do it.
 *
 * space-between only centers a middle child when the two outside it are the same
 * width, and these never are: "Preview" and "Info" are different words in every
 * locale, and one of them loses its label entirely below the breakpoint further
 * down. So undo/redo sat off-center by half the difference, drifting as the
 * language changed — and drifting AGAIN, visibly, at the width where the labels
 * disappear.
 *
 * The two outer buttons share the leftover space equally instead, which pins the
 * middle to the true center of the bar whatever they contain, and each one keeps
 * its own alignment so Preview still reads from the left edge and Info to the
 * right. `flex-basis: 0` rather than auto, so it is the SPACE that is shared and
 * not the content that is stretched; the 44px min-width on the button still
 * holds the floor.
 */
.stp-capture__topbar > .stp-capture__chrome-button--preview,
.stp-capture__topbar > .stp-capture__chrome-button--info {
	flex: 1 1 0;
}

.stp-capture__topbar > .stp-capture__chrome-button--preview {
	justify-content: flex-start;
}

.stp-capture__topbar > .stp-capture__chrome-button--info {
	justify-content: flex-end;
}

.stp-capture__topbar[hidden] {
	display: none;
}

/*
 * White at 60%, not the accent: the accent is a light blue on a dark navy and
 * the focus ring is the one thing that must be unmistakable at a glance.
 */
.stp-capture__topbar :focus-visible {
	outline: 2px solid var( --stp-chrome-ink );
	outline-offset: 2px;
	box-shadow: 0 0 0 3px var( --stp-light-28 );
}

/* Its natural width, so the space either side of it is what gets shared. */
.stp-capture__chrome-history {
	flex: 0 0 auto;
	display: flex;
	align-items: center;
	gap: 2px;
}

.stp-capture__chrome-button {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 6px;
	/* Mobile ship gate: a 44px target on a bar a thumb reaches for. */
	min-width: 44px;
	min-height: 44px;
	padding: 4px 10px;
	font: inherit;
	font-size: 0.8125em;
	line-height: 1.2;
	color: var( --stp-chrome-ink );
	background: transparent;
	border: 1px solid transparent;
	border-radius: var( --stp-radius );
	cursor: pointer;
}

/*
 * Muted rather than gone. An undo button that vanishes at the start of the
 * stack takes the middle of the bar with it and moves everything else; one that
 * is visibly unavailable tells the customer the history exists at all.
 */
.stp-capture__chrome-button[disabled] {
	opacity: 0.38;
	cursor: default;
}

.stp-capture__chrome-button:hover:not( [disabled] ) {
	background: var( --stp-light-12 );
}

.stp-capture__chrome-button.is-on {
	background: var( --stp-light-18 );
	border-color: var( --stp-light-50 );
}

.stp-capture__chrome-icon {
	flex: 0 0 auto;
	width: 20px;
	height: 20px;
}

.stp-capture__chrome-label {
	white-space: nowrap;
}

/*
 * NARROW PHONES. Below this width the words go and the icons stay, which is the
 * same trade the parity row already makes — the accessible name is on the button
 * either way (aria-label), so nothing is lost to anyone who was not reading the
 * pixels.
 *
 * 420px, NOT 360px. The old figure was arrived at from the English labels and
 * nothing else: "Preview" and "Info" are short, and four controls carrying them
 * do fit a 360px bar. They are not short everywhere — German ("Vorschau"),
 * French ("Aperçu"), Spanish ("Vista previa"), and any locale whose word for
 * Info is a phrase — and on the common 375px and 390px phones those overflowed
 * the bar, which is a flex row: the labels did not wrap, they squeezed the
 * history group off center and pushed Info against the edge. Above 420px there
 * is room for a translated pair with the middle group intact, and below it the
 * icons alone are the same four unambiguous targets.
 */
@media ( max-width: 419px ) {

	.stp-capture__chrome-label {
		position: absolute;
		width: 1px;
		height: 1px;
		padding: 0;
		margin: -1px;
		overflow: hidden;
		clip: rect( 0, 0, 0, 0 );
		white-space: nowrap;
		border: 0;
	}
}

/*
 * The Info sheet: the stage instruction and the standing fidelity caveat, which
 * are strings this file already shows. No prose is written here.
 */
.stp-capture__info {
	display: flex;
	flex-direction: column;
	gap: var( --stp-gap-sm );
}

.stp-capture__info-line {
	margin: 0;
	color: var( --stp-ink-muted );
	font-size: 0.875em;
}

/* -------------------------------------------------------------------------
 * THE FACTS SHEET
 *
 * One builder (buildFactsPanel), two homes: a studio panel in visual mode and
 * a collapsible region under the price in quick capture. These rules are
 * written for the CONTENT and know nothing about which home it is in, which is
 * what lets the two presentations stay one thing.
 *
 * NO TABLES ANYWHERE IN HERE, and that is a hard constraint rather than a
 * preference: 430x932 is the primary surface, and a table cannot reflow at
 * 430px — a two-column table with "Estimated stitch count" on the left either
 * wraps into a column two characters wide or scrolls the page sideways. Every
 * pair is a `dl` on a 2-column grid instead: the label takes the space it
 * needs and wraps under itself, the value stays pinned to the right edge.
 * ---------------------------------------------------------------------- */

.stp-capture__facts {
	display: flex;
	flex-direction: column;
	gap: var( --stp-gap );
}

.stp-capture__facts-sec {
	display: flex;
	flex-direction: column;
	gap: var( --stp-gap-sm );
}

.stp-capture__facts-h {
	margin: 0;
	font-size: 0.9375em;
	font-weight: 600;
	color: var( --stp-ink );
}

/*
 * Label left, value right. `1fr auto` rather than two equal columns: the value
 * is a short figure and the label is prose, so the prose gets the slack — at
 * 430px "Screens to burn" and "5 (4 + underbase)" sit on one line, and a
 * translation twice as long wraps the label without ever pushing the number
 * off the edge.
 */
.stp-capture__facts-dl {
	display: grid;
	grid-template-columns: 1fr auto;
	gap: 4px 12px;
	margin: 0;
	font-size: 0.9375em;
}

.stp-capture__facts-dt {
	margin: 0;
	color: var( --stp-ink-muted );
	min-width: 0;
}

.stp-capture__facts-dd {
	margin: 0;
	text-align: right;
	font-weight: 600;
	/*
	 * Lining figures of equal width, so a column of numbers reads as a column.
	 * Harmless where the font has no such set — the browser falls back to
	 * whatever it has.
	 */
	font-variant-numeric: tabular-nums;
}

/*
 * The caveat under a figure ("An estimate. We confirm after digitizing.").
 * A second <dd> under the same <dt>, spanning both columns — which is what a
 * definition list is for, and is why this needs no wrapper element.
 */
.stp-capture__facts-note {
	grid-column: 1 / -1;
	margin: 0;
	color: var( --stp-ink-muted );
	font-size: 0.8125em;
	line-height: 1.45;
}

/*
 * The THIRD `<dd>` under a row: whatever the caller wants to hang there as a
 * node rather than as a string. Only the review summary uses it today (the
 * color swatches and the placement's price), and like the note above it, it
 * spans both columns and needs no wrapper element of its own.
 */
.stp-capture__facts-extra {
	grid-column: 1 / -1;
	margin: 0;
}

/* -------------------------------------------------------------------------
 * THE REVIEW STEP (Wave 1)
 *
 * A studio panel like the facts sheet, and deliberately built out of the facts
 * sheet's own parts: the sections, the headings, the `dl` grid and the money
 * block are all the rules above. What is here is only the things a summary has
 * that a facts sheet does not — a row per placement, a color swatch, and the
 * price of that one placement.
 *
 * NOTHING IN HERE HAS A SIZE THAT DEPENDS ON THE STAGE. The panel overlays the
 * canvas and never reflows it (see the sheet's own block), so no rule in this
 * section can move a zone rect or re-fit a text item.
 * ---------------------------------------------------------------------- */

.stp-capture__review,
.stp-capture__review-body {
	min-height: 0;
}

/*
 * The summary is where focus lands when the step opens (studioFocusIn), so it
 * has to show that it has been landed on. Same treatment, same reasoning, as
 * the facts region: it is a summary rather than a form control, so it takes a
 * ring rather than a control's outline.
 */
.stp-capture__review:focus {
	outline: none;
	box-shadow: var( --stp-focus );
	border-radius: var( --stp-radius-sm );
}

.stp-capture__review-body {
	display: flex;
	flex-direction: column;
	gap: var( --stp-gap );
}

/*
 * The colors and the price for one placement, on one line: swatches left,
 * money right. `space-between` with `flex-wrap`, so three thread colors and a
 * price still resolve at 430px by wrapping rather than by squeezing.
 */
.stp-capture__review-meta {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 4px var( --stp-gap-sm );
	margin-top: 2px;
}

.stp-capture__review-colors {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 4px var( --stp-gap-sm );
	min-width: 0;
}

.stp-capture__review-color {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	min-width: 0;
	color: var( --stp-ink-muted );
	font-size: 0.8125em;
}

/*
 * The swatch. A 14px disc with a hairline ring, because a light thread on a
 * light panel is otherwise a color with no edge — the same reason the picker's
 * swatches carry one. It is `aria-hidden` in the markup: the code and the name
 * beside it are the color's name, and a dot is not a thing to read out.
 *
 * `flex: none` so it can never be squeezed into an ellipse by a long name.
 */
.stp-capture__review-dot {
	flex: none;
	width: 14px;
	height: 14px;
	border: 1px solid var( --stp-border );
	border-radius: 50%;
}

.stp-capture__review-color-name {
	min-width: 0;
	overflow-wrap: break-word;
}

.stp-capture__review-price {
	margin-left: auto;
	font-weight: 600;
	font-variant-numeric: tabular-nums;
}

/*
 * The two borrowed footer fields at the bottom of the panel (the note and the
 * proof choice — openStudioPanel lends them, in that order, after the summary
 * container). They are the FOOTER's nodes, so they arrive with the footer's
 * styling and the only thing missing is a rule that separates them from the
 * summary above.
 *
 * Selected off the summary container rather than off the sheet body, so the
 * line is drawn exactly once and only where there is something above it to be
 * separated from — the adjacent sibling combinator is the whole guard.
 */
.stp-capture__review + .stp-capture__field--note {
	margin-top: var( --stp-gap );
	padding-top: var( --stp-gap );
	border-top: 1px solid var( --stp-border );
}

.stp-capture__field--note + .stp-capture__field--proof {
	margin-top: var( --stp-gap-sm );
}

/*
 * The advisories. AMBER, NEVER RED — see --stp-notice-bg. A left border rather
 * than a filled block: these sit inside a panel a customer opened deliberately,
 * and a wall of color there reads as an error state.
 */
.stp-capture__facts-warn {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: var( --stp-gap-sm );
}

.stp-capture__facts-warn-item {
	margin: 0;
	padding: 6px 0 6px 12px;
	border-left: 3px solid var( --stp-notice-edge );
	color: var( --stp-ink );
	font-size: 0.875em;
	/*
	 * 1.45 because these are two and three lines of prose on a phone, not
	 * labels; and `text-wrap: pretty` where the browser has it, so a warning
	 * does not end on a one-word last line. Both degrade to nothing.
	 */
	line-height: 1.45;
	text-wrap: pretty;
}

.stp-capture__facts-money {
	display: flex;
	flex-direction: column;
	gap: var( --stp-gap-sm );
}

/*
 * The price breakdown. The same grid as the figures above it, deliberately: a
 * customer scanning the sheet meets one column of labels and one of numbers,
 * not two layouts that happen to look similar.
 */
.stp-capture__bd {
	display: grid;
	grid-template-columns: 1fr auto;
	gap: 4px 12px;
	margin: 0;
	font-size: 0.9375em;
}

.stp-capture__bd-dt {
	margin: 0;
	color: var( --stp-ink-muted );
	min-width: 0;
}

.stp-capture__bd-dd {
	margin: 0;
	text-align: right;
	font-variant-numeric: tabular-nums;
}

/* The next quantity break: an opportunity, so it reads as one. */
.stp-capture__facts-tier {
	margin: 0;
	color: var( --stp-success );
	font-size: 0.875em;
	line-height: 1.45;
}

.stp-capture__facts-disclaimer {
	margin: 0;
	color: var( --stp-ink-muted );
	font-size: 0.8125em;
	line-height: 1.4;
}

/*
 * The sheet's own container inside the studio panel. The panel scrolls; this
 * only has to not add a second scroll box inside it.
 */
.stp-capture__facts-body {
	min-height: 0;
}

/*
 * The MODAL presentation's collapsible region, under the price.
 *
 * A FOOTER CHILD since 0.10.0 (see buildModal), so it has lines in every map
 * below. It used to be a child of .stp-capture__price, which put it inside a
 * block refreshPrice() hides whenever there is no estimate to show — and the
 * warning strip stays up through exactly that state, so pressing "2 things to
 * check" opened a `display: none` region and `aria-expanded` said "true" about
 * nothing. Its top rule is what separates it from the number it belongs to;
 * without one the sheet reads as more price.
 */
.stp-capture__facts-region {
	margin-top: var( --stp-gap-sm );
	padding-top: var( --stp-gap-sm );
	border-top: 1px solid var( --stp-border );
}

.stp-capture__facts-region[hidden] {
	display: none;
}

/*
 * A disclosure is not a form control and should not take a focus ring shaped
 * like one — but it IS focused programmatically when it opens, so it needs to
 * show where the customer has been put.
 */
.stp-capture__facts-region:focus {
	outline: none;
	box-shadow: var( --stp-focus );
	border-radius: var( --stp-radius-sm );
}

/* -------------------------------------------------------------------------
 * Error region
 * ---------------------------------------------------------------------- */

.stp-capture__error {
	margin: 0;
	padding: var( --stp-gap-sm ) var( --stp-gap );
	color: var( --stp-danger );
	background: var( --stp-danger-08 );
	border-bottom: 1px solid var( --stp-danger-25 );
	font-size: 0.9375em;
}

.stp-capture__error[hidden] {
	display: none;
}

/*
 * The server can refuse an add for more than one reason at a time; they are
 * listed rather than run together into one sentence, because each one is a
 * separate thing the customer has to go and fix.
 */
.stp-capture__error-list {
	margin: 0;
	padding-left: 1.25em;
}

.stp-capture__error-list li + li {
	margin-top: 4px;
}

/* -------------------------------------------------------------------------
 * Notice region (not a failure — currently "you are editing a saved design")
 * ---------------------------------------------------------------------- */

.stp-capture__notice {
	margin: 0;
	padding: var( --stp-gap-sm ) var( --stp-gap );
	color: var( --stp-notice );
	background: var( --stp-notice-08 );
	border-bottom: 1px solid var( --stp-notice-25 );
	font-size: 0.9375em;
}

.stp-capture__notice[hidden] {
	display: none;
}

/* -------------------------------------------------------------------------
 * Capture-mode pills (quick / visual placement)
 *
 * Rendered only when the template offers both, so this bar is either two
 * buttons or absent — never one button pretending to be a choice.
 * ---------------------------------------------------------------------- */

.stp-capture__capture-modes {
	display: flex;
	flex-wrap: wrap;
	gap: var( --stp-gap-sm );
	padding: var( --stp-gap ) var( --stp-gap ) 0;
}

.stp-capture__capture-modes[hidden] {
	display: none;
}

/* -------------------------------------------------------------------------
 * Screen-reader-only region (throttled placement announcements)
 * ---------------------------------------------------------------------- */

.stp-capture__sr {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect( 0, 0, 0, 0 );
	white-space: nowrap;
	border: 0;
}

/* -------------------------------------------------------------------------
 * Body: stage + panel
 * ---------------------------------------------------------------------- */

.stp-capture__body {
	display: flex;
	flex-direction: column;
	gap: var( --stp-gap );
	padding: var( --stp-gap );
	overflow-y: auto;
}

.stp-capture__views {
	display: flex;
	flex-wrap: wrap;
	gap: var( --stp-gap-sm );
	margin-bottom: var( --stp-gap-sm );
}

.stp-capture__views[hidden] {
	display: none;
}

.stp-capture__view {
	padding: 6px 12px;
	font-size: 0.875em;
	color: var( --stp-ink );
	background: var( --stp-surface-alt );
	border: 1px solid var( --stp-border );
	border-radius: 999px;
	cursor: pointer;
}

/*
 * aria-pressed, not aria-selected: the view switcher is a group of toggle
 * buttons, not a tablist — every view paints the same canvas, so there are no
 * tabpanels for tab semantics to point at. Matches the method pills.
 */
.stp-capture__view[aria-pressed="true"] {
	color: var( --stp-accent-contrast );
	background: var( --stp-accent );
	border-color: var( --stp-accent );
}

/*
 * The labeled pills name the views and stay in quick capture, where there is
 * no garment to read a bare "2/4" against. Visual mode gets the floating pager
 * over the canvas instead — one control instead of a row that grows with the
 * template, and the garment keeps the width.
 */
.stp-capture.is-visual .stp-capture__views {
	display: none;
}

.stp-capture__canvas {
	position: relative;
	overflow: hidden;
	background: var( --stp-surface-alt );
	border: 1px solid var( --stp-border );
	border-radius: var( --stp-radius-sm );
}

/* -------------------------------------------------------------------------
 * View pager (visual mode) — floats over the bottom center of the garment
 *
 * IT USED TO SIT IN THE TOP-RIGHT CORNER, and that is where the zones are.
 * Every garment template this plugin ships for — a cap, a tee, a polo — is
 * photographed with its decoratable areas in the upper half of the frame:
 * front center, left and right chest, the back yoke. A floating pill in that
 * corner sat ON TOP of them. It swallowed taps meant for a right-chest hotspot,
 * it covered a decoration the customer had just placed there, and on a phone it
 * was the far corner from the thumb holding the device.
 *
 * The hem is the one part of a garment photograph that is reliably empty, the
 * pill is a fixed height so it cannot grow back up into the artwork, and bottom
 * center is where a thumb already is. Nothing else about it changes: same
 * stacking context, same z-index above the item layer, same disabled-at-the-ends
 * behavior.
 * ---------------------------------------------------------------------- */

.stp-capture__pager {
	position: absolute;
	bottom: 8px;
	/* Centered on the GARMENT, not on the stage: the pager is a child of the
	 * canvas, so 50% is half the image's width whatever the stage does with the
	 * space around it. */
	left: 50%;
	transform: translateX( -50% );
	/*
	 * Above the hotspot layer (which contains a stacking context capped at its
	 * own auto slot) and above the item layer, so a decoration dragged over the
	 * hem passes under the pager rather than over it.
	 */
	z-index: 40;
	display: flex;
	align-items: center;
	gap: 2px;
	padding: 2px;
	color: var( --stp-ink );
	background: var( --stp-surface );
	border: 1px solid var( --stp-border );
	border-radius: 999px;
	box-shadow: 0 2px 10px var( --stp-shade-18 );
}

.stp-capture__pager[hidden] {
	display: none;
}

/*
 * An open studio sheet and the pager both claim the bottom-center of a phone,
 * and the sheet used to simply cover it (HANDOFF known-cosmetic). While a
 * sheet is up (capture.js stamps `.is-sheet-open` on the root for exactly its
 * lifetime) the pager fades and stops taking taps; switching views mid-panel
 * was never a meaningful action. Opacity rather than `hidden`, so closing the
 * sheet brings it back without re-entering layout.
 *
 * Phones only: from 720px the sheet is a LEFT DRAWER (see the studio desktop
 * block) and the pager's bottom-center is unobstructed — hiding it there
 * would remove a working control to solve a collision that cannot happen.
 */
@media ( max-width: 719.98px ) {

	.stp-capture.is-sheet-open .stp-capture__pager {
		opacity: 0;
		pointer-events: none;
		transition: opacity 0.15s ease-out;
	}
}

.stp-capture__pager-button {
	display: flex;
	align-items: center;
	justify-content: center;
	/* The same 44px the drag handles use — this sits on the same canvas. */
	width: var( --stp-handle-hit );
	height: var( --stp-handle-hit );
	padding: 0;
	color: inherit;
	background: transparent;
	border: 0;
	border-radius: 50%;
	cursor: pointer;
}

.stp-capture__pager-button:hover:not( [disabled] ) {
	background: var( --stp-surface-alt );
}

/* Disabled at the ends of the run: the pager does not wrap, and saying so is
 * how a customer knows they have seen every view. */
.stp-capture__pager-button[disabled] {
	opacity: 0.35;
	cursor: default;
}

.stp-capture__pager-arrow {
	width: 20px;
	height: 20px;
}

.stp-capture__pager-glyph {
	width: 16px;
	height: 16px;
	color: var( --stp-ink-muted );
}

/* Tabular figures so the pill does not resize as the count passes 9 to 10. */
.stp-capture__pager-count {
	min-width: 3ch;
	font-size: 0.8125em;
	font-variant-numeric: tabular-nums;
	text-align: center;
}

/* -------------------------------------------------------------------------
 * THE EMPTY-DESIGN INVITATION (Wave 1)
 *
 * "What would you like on it?" plus the two add actions, floating over the top
 * of the garment for exactly as long as there is nothing on it.
 *
 * ABSOLUTE, LIKE THE PAGER, AND FOR A HARDER REASON THAN TIDINESS. The canvas
 * shrink-wraps the view image and the image is capped by --stp-stage-h; a child
 * in the normal flow would grow the canvas — which is the coordinate frame every
 * zone rect and every stored placement is a percentage OF (SPEC-SCHEMA) — and
 * would push the footer's action bar back below the fold. Out of flow, it can do
 * neither. No rule in this block gives the canvas, the image or the stage a size.
 *
 * TOP, not center. The zones are already outlined while the design is empty
 * (.is-empty-design, 0.8.0) and most of them are in the upper-middle of a
 * garment photograph; a card in the middle of the frame would sit on top of the
 * front-center outline it is inviting the customer towards. The top strip is the
 * one part of a cap or a tee shot that is reliably empty.
 *
 * DISPLAY: NONE AT REST, which is also what keeps its two buttons out of the
 * focus order (focusables() filters on offsetParent). Four classes decide it and
 * all four are already maintained by capture.js: the studio layout, an empty
 * design, no proof lens, no open sheet.
 * ---------------------------------------------------------------------- */

.stp-capture__invite {
	display: none;
}

.stp-capture.is-studio.is-empty-design .stp-capture__invite {
	position: absolute;
	top: 8px;
	left: 50%;
	transform: translateX( -50% );
	/*
	 * Above the hotspot layer and the item layer, below nothing — the same slot
	 * the pager takes, and for the same reason: it is chrome over the garment,
	 * not something on it.
	 */
	z-index: 40;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 6px;
	max-width: calc( 100% - 16px );
	padding: 8px 10px;
	color: var( --stp-ink );
	background: var( --stp-surface );
	border: 1px solid var( --stp-border );
	border-radius: var( --stp-radius );
	box-shadow: 0 2px 10px var( --stp-shade-18 );
	text-align: center;
	/*
	 * TAPS PASS THROUGH THE CARD TO THE ZONES UNDER IT; only its buttons take
	 * them (the buttons restore pointer-events below — the pager pattern).
	 * 13th-round BLOCKER: without this, on the max-height:760px stage
	 * (34vh — iPhone SE portrait, any phone in landscape) the card covered up
	 * to 81% of the canvas and the back view's only zone was unreachable by
	 * tap — in exactly the empty-design state where tapping a zone is the
	 * point. "The top strip is reliably empty" was false against the repo's
	 * own reference geometry (back_yoke starts at 24% of canvas height).
	 */
	pointer-events: none;
}

.stp-capture__invite-button {
	pointer-events: auto;
}

.stp-capture__invite-lead {
	margin: 0;
	font-size: 0.8125em;
	font-weight: 600;
	line-height: 1.3;
}

.stp-capture__invite-actions {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 6px;
}

/*
 * The two actions. Deliberately NOT the accent-filled primary style: the one
 * filled button on this card is Add to cart, and a garment wearing two more of
 * them tells a customer nothing about which is the way forward.
 *
 * 36px tall rather than the 44 the canvas handles use — this is a floating card
 * on top of the garment, and a 44px pair pushed it over the front-center zone at
 * 375px. Above the 24px floor ARCHITECTURE sets for a tap target, and it is not
 * a target a customer has to hit under time pressure or with a moving finger.
 */
.stp-capture__invite-button {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	min-height: 36px;
	padding: 6px 12px;
	font: inherit;
	font-size: 0.8125em;
	font-weight: 600;
	color: var( --stp-ink );
	background: var( --stp-surface-alt );
	border: 1px solid var( --stp-border );
	border-radius: 999px;
	cursor: pointer;
}

.stp-capture__invite-button:hover {
	border-color: var( --stp-border-strong );
}

.stp-capture__invite-button[hidden] {
	display: none;
}

.stp-capture__invite-button .stp-capture__tool-icon {
	width: 16px;
	height: 16px;
}

/*
 * It goes when the proof lens goes on — there is nothing to add to under a lens
 * that exists to show the finished thing — and while a sheet is up, because the
 * customer is already inside the panel it would have opened.
 *
 * BOTH RULES REPEAT THE OTHER TWO CLASSES ON PURPOSE. The rule that shows
 * the invitation is `.is-studio.is-empty-design` — 0-3-0 — so a 0-2-0 override
 * here would lose to it at every width and the invitation would sit on the
 * proof view. Specificity is the mechanism, so it is stated rather than assumed.
 */
.stp-capture.is-studio.is-empty-design.is-preview .stp-capture__invite {
	display: none;
}

/*
 * DISPLAY, NOT THE PAGER'S OPACITY, and the difference is the focus order.
 *
 * `opacity: 0` leaves an element laid out, in the accessibility tree and — as
 * far as focusables() is concerned, which asks `offsetParent !== null` — in the
 * modal's focus trap. The pager gets away with that because its buttons stay
 * meaningful behind a panel; two invitation buttons a keyboard customer can
 * reach but not see are not the same thing, and there is nothing to fade in a
 * transition nobody asked for. Specificity is stated rather than assumed: the
 * rule that shows the invitation carries three classes.
 */
.stp-capture.is-studio.is-empty-design.is-sheet-open .stp-capture__invite {
	display: none;
}

.stp-capture__image {
	display: block;
	width: 100%;
	height: auto;
}

.stp-capture__zones {
	position: absolute;
	inset: 0;
	/*
	 * A STACKING CONTEXT, deliberately, and the value must stay 0.
	 *
	 * paintZones() writes a per-hotspot z-index derived from zone area (small
	 * zone on top, so the left chest inside a full front is still tappable),
	 * and those values run as high as 1000. Without a context here they would
	 * compete with .stp-capture__items in the canvas's stacking order and win,
	 * putting the hotspot layer over the decorations the customer is dragging.
	 * Contained here, the whole hotspot layer keeps its single auto-height slot
	 * under the placement layer and the ranking only orders hotspots against
	 * each other, which is all it was ever meant to do.
	 */
	z-index: 0;
}

/*
 * Zone hotspots are positioned with the percentages stored on the template.
 * There is no correction pass anywhere in this stylesheet or in capture.js —
 * if a zone looks wrong, the stored geometry is wrong (capture-integrity D1).
 */
.stp-capture__zone {
	position: absolute;
	display: flex;
	align-items: center;
	justify-content: center;
	/*
	 * Visual floor only — the stored percentages are untouched (D1). A 1%-wide
	 * zone on a 320px phone canvas is a 3px tap target; the hotspot is grown to
	 * something a finger can hit while the geometry it represents stays exactly
	 * as configured.
	 */
	min-width: 24px;
	min-height: 24px;
	padding: 0;
	color: var( --stp-ink );
	background: var( --stp-light-16 );
	border: 2px dashed var( --stp-zone-outline );
	border-radius: var( --stp-radius-sm );
	cursor: pointer;
}

/*
 * The TAP target outgrows the VISUAL target by 10px a side (an invisible
 * pseudo-element participates in the button's hit-testing). The 24px visual
 * floor above is honest about where the zone is; this is the concession to
 * real thumbs — a small left-chest zone gains ~20px of effective target with
 * not one stored percentage or painted pixel moved (capture-integrity D1).
 * Adjacent zones can overlap by that margin; paintZones() already ranks the
 * smaller zone's hotspot on top, so the harder target wins the contested
 * strip.
 */
.stp-capture__zone::before {
	content: "";
	position: absolute;
	top: -10px;
	right: -10px;
	bottom: -10px;
	left: -10px;
}

.stp-capture__zone:hover,
.stp-capture__zone:focus-visible {
	background: var( --stp-light-34 );
	border-style: solid;
}

.stp-capture__zone.is-filled {
	border-color: var( --stp-accent );
	border-style: solid;
	background: var( --stp-accent-tint-14 );
}

.stp-capture__zone-label {
	padding: 2px 6px;
	font-size: 11px;
	line-height: 1.2;
	text-align: center;
	background: var( --stp-light-85 );
	border-radius: 3px;
}

/*
 * How many decorations are in this zone. Only rendered above one — a badge
 * reading "1" on every filled hotspot says nothing — and pinned to the corner
 * rather than laid out in flow, so it does not push the zone's own label off
 * center. The count is repeated in the button's accessible name, so the badge
 * itself is aria-hidden and is pure signal for the eye.
 */
.stp-capture__zone-count {
	position: absolute;
	top: -8px;
	right: -8px;
	min-width: 18px;
	height: 18px;
	padding: 0 4px;
	font-size: 11px;
	font-weight: 700;
	line-height: 18px;
	text-align: center;
	color: var( --stp-accent-contrast );
	background: var( --stp-accent );
	border: 1px solid var( --stp-halo );
	border-radius: 999px;
	box-sizing: border-box;
}

.stp-capture__zone-count[hidden] {
	display: none;
}

/* -------------------------------------------------------------------------
 * Zones are INVISIBLE AT REST in visual mode
 *
 * The single biggest difference between a product page and a tool: a garment
 * covered in dashed rectangles reads as software, and the customer came to see
 * their name on a cap. So in visual mode the outlines, the labels and the count
 * badges are painted out — and NOTHING ELSE CHANGES. The hotspots are still
 * buttons of exactly the size the stored geometry gives them (no rule here
 * touches a percentage — capture-integrity D1), still tappable, still in the tab
 * order, still carrying their accessible names.
 *
 * They come back for five reasons and no others:
 *
 *   1. a one-off pulse when the card first becomes interactive, so the customer
 *      is shown where they may go (JS adds .is-zones-pulse once and drops it on
 *      the animation's own end event — no timer);
 *   2. .is-hot — the zone a gesture is currently bounded by, for the length of
 *      that gesture, because that boundary is the explanation for what the item
 *      is doing;
 *   3. :hover / :focus-visible on the hotspot itself — a pointer or a keyboard
 *      arriving on a target has to be told it is a target;
 *   4. .is-areas — every zone, while the Areas panel is the thing on screen,
 *      because that panel's whole subject is the areas;
 *   5. .is-empty-design — every zone, quietly, while NOTHING is decorated yet.
 *      The pulse of reason 1 is gone in 1.2s, and a customer who loaded the
 *      page while reading elsewhere met a plain photograph with no visible way
 *      in. The rest state returns the moment the first decoration lands
 *      (refreshLauncher() drops the class), which is exactly when the garment
 *      has to stop looking like software again.
 *
 * Quick capture is untouched: it has no stage to keep clean and the outlines
 * ARE its interface.
 * ---------------------------------------------------------------------- */

.stp-capture.is-visual .stp-capture__zone,
.stp-capture.is-visual .stp-capture__zone.is-filled {
	background: transparent;
	border-color: transparent;
	transition:
		background-color 0.18s ease-out,
		border-color 0.18s ease-out;
}

.stp-capture.is-visual .stp-capture__zone-label,
.stp-capture.is-visual .stp-capture__zone-count {
	opacity: 0;
	transition: opacity 0.18s ease-out;
}

/*
 * Reason 5: the empty design. Softer than the interaction states below — a
 * dashed invitation, not a highlight — because these zones are scenery until
 * the customer commits to one. Labels ride along at full opacity; an outline
 * with no name is a puzzle, not an affordance.
 *
 * PLACED ABOVE the hover/focus/.is-hot/.is-areas reveal rules on purpose: the
 * selectors tie at (0,4,0), so source order decides, and the reveal states
 * must still win while the design is empty — a finger arriving on a zone
 * upgrades the soft invitation to the full highlight (8th-round finding: the
 * first draft sat below them and deadened reveals exactly for first-timers).
 */
.stp-capture.is-visual.is-empty-design .stp-capture__zone {
	background: var( --stp-light-10 );
	border-color: var( --stp-zone-outline );
	border-style: dashed;
}

.stp-capture.is-visual.is-empty-design .stp-capture__zone-label {
	opacity: 1;
}

.stp-capture.is-visual .stp-capture__zone:hover,
.stp-capture.is-visual .stp-capture__zone:focus-visible,
.stp-capture.is-visual .stp-capture__zone.is-hot,
.stp-capture.is-visual.is-areas .stp-capture__zone {
	background: var( --stp-light-22 );
	border-color: var( --stp-zone-outline );
	border-style: solid;
}

/* A filled zone keeps its accent identity when it is shown at all. */
.stp-capture.is-visual .stp-capture__zone.is-filled:hover,
.stp-capture.is-visual .stp-capture__zone.is-filled:focus-visible,
.stp-capture.is-visual .stp-capture__zone.is-filled.is-hot,
.stp-capture.is-visual.is-areas .stp-capture__zone.is-filled {
	background: var( --stp-accent-tint-14 );
	border-color: var( --stp-accent );
}

.stp-capture.is-visual .stp-capture__zone:hover .stp-capture__zone-label,
.stp-capture.is-visual .stp-capture__zone:focus-visible .stp-capture__zone-label,
.stp-capture.is-visual .stp-capture__zone.is-hot .stp-capture__zone-label,
.stp-capture.is-visual.is-areas .stp-capture__zone .stp-capture__zone-label,
.stp-capture.is-visual .stp-capture__zone:hover .stp-capture__zone-count,
.stp-capture.is-visual .stp-capture__zone:focus-visible .stp-capture__zone-count,
.stp-capture.is-visual .stp-capture__zone.is-hot .stp-capture__zone-count,
.stp-capture.is-visual.is-areas .stp-capture__zone .stp-capture__zone-count {
	opacity: 1;
}

/*
 * The introduction. One run, and the class that carries it is removed by JS on
 * this animation's own `animationend` — so it cannot replay when a view switch
 * rebuilds every hotspot, and there is no timer deciding when it is over.
 *
 * DRAWN ON A PSEUDO-ELEMENT, not on the hotspot's own border, and that is a
 * correctness point rather than a style one. An animation beats every normal
 * declaration in the cascade for as long as it is applied — so a pulse that
 * animated `border-color` would hold the four reveal rules above closed while
 * it ran, and a browser that PAUSES it (a background tab is the ordinary case)
 * would hold them closed indefinitely: hover, focus, a drag and the Areas panel
 * would all silently stop showing the zone. On its own layer it cannot reach
 * them; a stuck pulse is an invisible overlay and nothing else.
 *
 * `opacity: 0` is the resting value as well as the first keyframe, because the
 * animation carries no fill-mode: when it ends the pseudo-element goes back to
 * the cascade, and the cascade has to say "not visible".
 */
@keyframes stp-zone-pulse {

	0%,
	100% {
		opacity: 0;
	}

	12%,
	68% {
		opacity: 1;
	}
}

.stp-capture.is-visual.is-zones-pulse .stp-capture__zone::after {
	content: "";
	position: absolute;
	inset: -2px;
	opacity: 0;
	background: var( --stp-light-22 );
	border: 2px dashed var( --stp-zone-outline );
	border-radius: var( --stp-radius-sm );
	pointer-events: none;
	animation: stp-zone-pulse 1.2s ease-in-out 1;
}

/* -------------------------------------------------------------------------
 * Preview — the customer proof view
 *
 * Everything the customer used to get here, gone: zone outlines, the selection
 * frame, the handles, and the pointer targets under all three. What is left is
 * the garment and the decorations on it, which is the thing being bought.
 *
 * A LENS, NOT A STATE. Nothing here is stored, nothing moves, and the payload
 * cannot tell the difference — capture.js clears the selection on the way in
 * (so the ring does not survive the class that hides it) and that is the whole
 * of what it touches.
 * ---------------------------------------------------------------------- */

.stp-capture.is-preview .stp-capture__zones {
	pointer-events: none;
}

.stp-capture.is-visual.is-preview .stp-capture__zone,
.stp-capture.is-visual.is-preview .stp-capture__zone:hover,
.stp-capture.is-visual.is-preview .stp-capture__zone:focus-visible,
.stp-capture.is-visual.is-preview .stp-capture__zone.is-hot {
	background: transparent;
	border-color: transparent;
}

.stp-capture.is-visual.is-preview .stp-capture__zone .stp-capture__zone-label,
.stp-capture.is-visual.is-preview .stp-capture__zone .stp-capture__zone-count {
	opacity: 0;
}

.stp-capture.is-preview .stp-capture__item {
	pointer-events: none;
	cursor: default;
}

.stp-capture.is-preview .stp-capture__item::before,
.stp-capture.is-preview .stp-capture__handle {
	display: none;
}

.stp-capture__hint {
	display: block;
	margin: var( --stp-gap-sm ) 0 0;
	color: var( --stp-ink-muted );
	font-size: 0.8125em;
}

/* -------------------------------------------------------------------------
 * Visual placement: items on the garment
 *
 * The layer is pointer-transparent and the items in it are not, so a tap on
 * bare garment still reaches the zone hotspot underneath it. Every offset in
 * here is a percentage of the ZONE box (the wrapper), which is the only
 * coordinate frame the stored placement knows about.
 * ---------------------------------------------------------------------- */

/*
 * The canvas clips in quick mode (nothing should ever paint outside the
 * garment) but must not in visual mode: the rotate handle deliberately hangs
 * above the item, and a zone near the top of the garment would otherwise have
 * a handle that is half there and impossible to grab.
 */
.stp-capture.is-visual .stp-capture__canvas {
	overflow: visible;
}

.stp-capture__items {
	position: absolute;
	inset: 0;
	pointer-events: none;
}

.stp-capture__zone-items {
	position: absolute;
	/* Handles hang outside the item, and a rotated item outside its own box. */
	overflow: visible;
	/*
	 * --stp-layer is written by orderItems() from the element's stored `layer`
	 * (0..11). A zone with several decorations stacks them here; the wrappers
	 * are also kept in record order in the DOM, which is what breaks a tie
	 * between two elements on the same layer (SPEC-SCHEMA).
	 *
	 * It arrives as a custom property rather than as an inline z-index so that
	 * the .is-lifted rule below can still win. An inline z-index outranks any
	 * stylesheet selector; this way both values sit in the cascade and the more
	 * specific class takes it, which is the whole point of the lift.
	 */
	z-index: var( --stp-layer, 0 );
}

/*
 * The selected element, lifted clear of its neighbors for as long as it is the
 * one being worked on. TEMPORARY and never stored — `layer` is the customer's
 * own choice and what the shop will produce, and it must not drift upwards
 * every time something is touched.
 */
.stp-capture__zone-items.is-lifted {
	z-index: 30;
}

.stp-capture__item {
	position: absolute;
	pointer-events: auto;
	/*
	 * THE PAGE MUST STILL SCROLL PAST A DECORATION THE CUSTOMER IS NOT EDITING.
	 *
	 * `touch-action: none` on every item was right for the modal and wrong the
	 * moment the card became a block in a product page. A decoration can cover a
	 * good part of the garment, the garment is most of the card, and the card is
	 * most of a phone screen — so a customer scrolling down the page who happened
	 * to start the swipe on a decoration found the page simply would not move.
	 * Nothing on screen explained it: the finger was on a photograph of a cap.
	 *
	 * `pan-y` gives the browser the vertical gesture and keeps the horizontal one,
	 * which is the correct trade for an item at rest — a decoration nobody has
	 * selected is scenery. The moment it IS the one being worked on (below) the
	 * whole gesture comes back, and that state is one tap away and visibly marked
	 * by the selection ring.
	 */
	touch-action: pan-y;
	cursor: move;
	/* The stored value is a percentage of the zone; JS writes it. */
	width: 80%;
	transform-origin: 50% 50%;
	-webkit-user-select: none;
	user-select: none;
}

/*
 * Selection frame. Drawn as a ring OUTSIDE the content so it never covers the
 * decoration, dashed accent over a light halo so it reads on a dark garment
 * and on a light one.
 */
.stp-capture__item::before {
	content: "";
	position: absolute;
	inset: -4px;
	border: 1px dashed var( --stp-accent );
	border-radius: 2px;
	box-shadow:
		0 0 0 1px var( --stp-halo ),
		inset 0 0 0 1px var( --stp-halo );
	pointer-events: none;
}

/*
 * ...and the selected item takes the gesture back in full.
 *
 * `is-selected` is the state a tap, a Tab or a click in the editor puts an
 * element into, and it is drawn with the emphasized ring below — so by the time
 * a customer is dragging, the thing under their finger is visibly the thing they
 * chose. `is-active` is the drag itself, which must not be interruptible by a
 * scroll it has already started outrunning.
 */
.stp-capture__item.is-selected,
.stp-capture__item.is-active {
	touch-action: none;
}

.stp-capture__item:focus {
	outline: none;
}

/*
 * SELECTED, ACTIVE and FOCUSED all draw the same emphasized ring.
 *
 * `is-selected` is the one that was missing: paintSelection() has always
 * written it, the tools row has always acted on it, and nothing on the garment
 * showed it — so on a zone with two decorations the customer could not tell
 * which one the Curve slider and the Delete button were about to change.
 * `is-active` (a drag in progress) and focus-visible are moments; selection is
 * a state that outlives both, and it earns the same ring.
 */
.stp-capture__item:focus-visible::before,
.stp-capture__item.is-selected::before,
.stp-capture__item.is-active::before {
	border-style: solid;
	border-width: 2px;
	box-shadow:
		0 0 0 2px var( --stp-halo ),
		inset 0 0 0 2px var( --stp-halo );
}

.stp-capture__item-text {
	display: block;
	width: 100%;
	line-height: 1;
	text-align: center;
}

/*
 * inline-block + nowrap is load-bearing, not cosmetic: capture.js reads this
 * span's offsetWidth at a reference font-size to derive the size that fills
 * the item exactly. Let it wrap and that measurement becomes meaningless.
 */
.stp-capture__item-line {
	display: inline-block;
	line-height: 1;
	white-space: nowrap;
}

.stp-capture__item-art {
	display: block;
	width: 100%;
	height: auto;
	-webkit-user-drag: none;
}

/*
 * The mirror sits INSIDE the rotated element rather than being multiplied into
 * its transform. scaleX(-1) then rotate(θ) is not rotate(θ) then scaleX(-1),
 * and the one a customer means by "flip" is "mirror the artwork, then turn the
 * whole thing" — which is what nesting produces, for free, in the right order.
 */
.stp-capture__item-art.is-flipped {
	transform: scaleX( -1 );
}

/*
 * Curved text. The SVG carries a viewBox sized to the glyphs actually drawn, so
 * it fills the item's width and takes whatever height its own aspect ratio
 * implies — which is how a curve makes the item's box taller without a single
 * pixel measurement in the arc code. overflow visible because the viewBox is
 * padded by eye, and a font with unusually long descenders should spill rather
 * than be cut.
 */
.stp-capture__item-svg {
	display: block;
	width: 100%;
	height: auto;
	overflow: visible;
}

/*
 * NO THREAD COLOR CHOSEN YET (capture.js toggles `.is-unset` on the item).
 *
 * The unstyled state inherited the page's ink, which on a dark garment photo
 * was black-on-black — a customer typed their name and saw nothing. Until a
 * color is picked the text is drawn in a neutral mid-gray inside a light
 * halo, a combination that reads on any fabric the shop photographs. This is
 * presentation only: the record carries no color until the customer chooses
 * one, and the validator still blocks add-to-cart without that choice. The
 * moment a real thread is picked, updateItem() removes the class and the
 * chosen hex takes over — including a black thread on a black cap, which is
 * then an honest preview of exactly what was ordered.
 */
.stp-capture__item.is-unset .stp-capture__item-text {
	color: var( --stp-ink-placeholder );
	text-shadow:
		0 0 1px var( --stp-light-95 ),
		0 1px 2px var( --stp-light-85 ),
		0 0 6px var( --stp-light-60 );
}

/* The arc's fill attribute is `currentColor` while unset, so the same color
 * arrives through `color`; the halo needs filter because text-shadow does not
 * reach SVG glyphs. */
.stp-capture__item.is-unset .stp-capture__item-svg {
	color: var( --stp-ink-placeholder );
	filter:
		drop-shadow( 0 0 1px var( --stp-light-95 ) )
		drop-shadow( 0 0 3px var( --stp-light-60 ) );
}

/*
 * Handles: a 44px touch target with a small visible dot, per the mobile-first
 * ship gate. The hit area is the element; the dot is a pseudo-element, so the
 * target does not have to look like a 44px blob on a phone-sized garment.
 */
.stp-capture__handle {
	position: absolute;
	display: block;
	width: var( --stp-handle-hit );
	height: var( --stp-handle-hit );
	pointer-events: auto;
	touch-action: none;
}

.stp-capture__handle::after {
	content: "";
	position: absolute;
	left: 50%;
	top: 50%;
	width: var( --stp-handle-dot );
	height: var( --stp-handle-dot );
	margin: calc( var( --stp-handle-dot ) / -2 ) 0 0 calc( var( --stp-handle-dot ) / -2 );
	background: var( --stp-accent );
	border: 2px solid var( --stp-halo );
	border-radius: 50%;
	box-sizing: border-box;
}

.stp-capture__handle--scale {
	right: calc( var( --stp-handle-hit ) / -2 );
	bottom: calc( var( --stp-handle-hit ) / -2 );
	cursor: nwse-resize;
}

/*
 * Above the item, and no further above it than it has to be: the hit area
 * hangs outside the garment image and the modal body is a scroll container,
 * so every extra pixel here is a pixel of handle a top-edge zone can push out
 * of view.
 */
.stp-capture__handle--rotate {
	left: 50%;
	top: calc( ( var( --stp-handle-hit ) + 6px ) * -1 );
	margin-left: calc( var( --stp-handle-hit ) / -2 );
	cursor: grab;
}

/* The tether, so the rotate dot reads as belonging to this item. */
.stp-capture__handle--rotate::before {
	content: "";
	position: absolute;
	left: 50%;
	top: 50%;
	width: 2px;
	height: calc( ( var( --stp-handle-hit ) / 2 ) + 6px );
	margin-left: -1px;
	background: var( --stp-accent );
	box-shadow: 0 0 0 1px var( --stp-halo );
}

/*
 * With something drawn in the zone, the zone's own name label is in the way of
 * the thing it names. The hotspot keeps its accessible name either way.
 */
.stp-capture.is-visual .stp-capture__zone.is-filled .stp-capture__zone-label {
	display: none;
}

/*
 * Fidelity honesty (SPEC-SCHEMA): the stage is an approximation drawn with the
 * customer's own screen metrics. Permanent while visual mode is on — not a
 * dismissible tooltip.
 */
.stp-capture__disclaimer {
	display: block;
	margin: 4px 0 0;
	color: var( --stp-ink-muted );
	font-size: 0.75em;
}

.stp-capture__disclaimer[hidden] {
	display: none;
}

/* -------------------------------------------------------------------------
 * Panel: methods + editors
 * ---------------------------------------------------------------------- */

.stp-capture__methods {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var( --stp-gap-sm );
	margin-bottom: var( --stp-gap );
}

.stp-capture__methods[hidden] {
	display: none;
}

.stp-capture__pill {
	padding: 6px 12px;
	font-size: 0.875em;
	color: var( --stp-ink );
	background: var( --stp-surface );
	border: 1px solid var( --stp-border-strong );
	border-radius: 999px;
	cursor: pointer;
}

.stp-capture__pill[aria-pressed="true"] {
	color: var( --stp-accent-contrast );
	background: var( --stp-accent );
	border-color: var( --stp-accent );
}

.stp-capture__editor {
	border: 1px solid var( --stp-border );
	border-radius: var( --stp-radius-sm );
	margin-bottom: var( --stp-gap-sm );
	background: var( --stp-surface );
}

.stp-capture__editor.is-filled {
	border-color: var( --stp-accent );
}

.stp-capture__editor-heading {
	margin: 0;
	font-size: 1em;
}

.stp-capture__editor-toggle {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: var( --stp-gap-sm );
	width: 100%;
	padding: 12px var( --stp-gap );
	font: inherit;
	text-align: left;
	color: var( --stp-ink );
	background: transparent;
	border: 0;
	cursor: pointer;
}

.stp-capture__editor-name {
	font-weight: 600;
}

.stp-capture__editor-status {
	flex: 1 1 auto;
	overflow: hidden;
	color: var( --stp-ink-muted );
	font-size: 0.8125em;
	text-align: right;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.stp-capture__editor-body {
	padding: 0 var( --stp-gap ) var( --stp-gap );
	border-top: 1px solid var( --stp-border );
}

.stp-capture__editor-body[hidden] {
	display: none;
}

.stp-capture__modes {
	display: flex;
	flex-wrap: wrap;
	gap: var( --stp-gap-sm );
	margin: var( --stp-gap ) 0;
}

.stp-capture__editor-actions {
	margin-top: var( --stp-gap );
}

/* -------------------------------------------------------------------------
 * Element repeater (several decorations in one zone)
 *
 * A zone whose max_items is 1 renders exactly one block with no heading and no
 * "add another", so this whole section is invisible on the templates that
 * existed before it — the separator only appears once there is something to
 * separate.
 * ---------------------------------------------------------------------- */

.stp-capture__element + .stp-capture__element {
	margin-top: var( --stp-gap );
	padding-top: var( --stp-gap );
	border-top: 1px dashed var( --stp-border );
}

.stp-capture__element-head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var( --stp-gap-sm );
	margin-top: var( --stp-gap );
}

.stp-capture__element-name {
	font-size: 0.8125em;
	font-weight: 700;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	color: var( --stp-ink-muted );
}

.stp-capture__add {
	margin-top: var( --stp-gap );
}

.stp-capture__add[hidden] {
	display: none;
}

/* -------------------------------------------------------------------------
 * Parity controls (duplicate, delete, center, flip, layer, arc)
 *
 * In the panel rather than floating over the garment, on purpose: a toolbar
 * pinned to a rotated 40px item on a phone overlaps the thing it acts on and
 * falls off the canvas near the edges. Here the controls are finger-sized,
 * tabbable and beside the fields that own the same element.
 * ---------------------------------------------------------------------- */

.stp-capture__element-tools {
	display: flex;
	flex-direction: column;
	gap: var( --stp-gap-sm );
	margin-top: var( --stp-gap );
}

.stp-capture__tools {
	display: flex;
	flex-wrap: wrap;
	gap: 6px;
}

.stp-capture__tools[hidden] {
	display: none;
}

.stp-capture__tool {
	display: flex;
	align-items: center;
	justify-content: center;
	/* Mobile ship gate: never below a 24px target, and comfortably above it. */
	width: 36px;
	height: 36px;
	padding: 0;
	color: var( --stp-ink );
	background: var( --stp-surface );
	border: 1px solid var( --stp-border-strong );
	border-radius: var( --stp-radius-sm );
	cursor: pointer;
}

.stp-capture__tool[hidden] {
	display: none;
}

.stp-capture__tool:hover {
	border-color: var( --stp-accent );
	color: var( --stp-accent-ink );
}

.stp-capture__tool[disabled] {
	opacity: 0.4;
	cursor: default;
}

.stp-capture__tool[aria-pressed="true"] {
	color: var( --stp-accent-contrast );
	background: var( --stp-accent );
	border-color: var( --stp-accent );
}

/* Destructive, and marked as such before it is pressed rather than after. */
.stp-capture__tool--delete:hover {
	color: var( --stp-danger );
	border-color: var( --stp-danger );
}

/*
 * The studio's Elements rows are the one place these 36px buttons are a FINGER
 * target rather than a mouse one, and the one place they are stacked in a
 * column: three of them — back, forward, delete — sit side by side at the right
 * edge of every row, one of them destructive, on a panel a customer scrolls
 * with the same thumb. 36px is under the 44px the rest of this file ships (the
 * rail buttons, the drag handles), and a mis-tap here deletes work.
 *
 * Scoped to a coarse pointer so a mouse keeps the compact row, and scoped to
 * the row so the parity toolbar inside an editor — seven buttons wrapping in a
 * narrow column — is not blown up with it.
 */
@media ( pointer: coarse ) {

	.stp-capture__row-acts .stp-capture__tool {
		width: 44px;
		height: 44px;
	}
}

/*
 * The 44px floor for the card's text-sized buttons on a phone: the mode and
 * method pills, the view switcher, the empty-design invitation and the quiet
 * footer links (Start over, Save design). Measured on staging at 390px they
 * were 30-36px tall — under the 44px every other finger target in this file
 * ships (rail, pager, chrome, drag handles) and under WCAG 2.5.5 / Apple HIG.
 *
 * Box only, not weight: min-height/min-width grow the hit area while the ink,
 * the font size and the padding stay what they were, so the links still read
 * as text and the invitation pills stay the quiet secondary style. Scoped to a
 * coarse pointer OR a phone-width card, so a desktop mouse keeps the compact
 * rows. Every selector is (0,2,0) so it out-ranks the (0,1,0) rules that set
 * the smaller floors above and below it (.stp-capture__startover's 24px)
 * regardless of source order. No zone hotspot is touched — those stay the
 * size their stored geometry gives them (capture-integrity D1).
 */
@media ( pointer: coarse ), ( max-width: 719.98px ) {

	.stp-capture .stp-capture__pill,
	.stp-capture .stp-capture__view,
	.stp-capture .stp-capture__invite-button,
	.stp-capture .stp-capture__link {
		min-width: 44px;
		min-height: 44px;
	}

	.stp-capture .stp-capture__invite-button {
		justify-content: center;
	}
}

/*
 * Strokes in currentColor: the icon follows the button's state — muted, accent
 * on hover, contrast when pressed — with no second set of color rules.
 */
.stp-capture__tool-icon {
	display: block;
	width: 20px;
	height: 20px;
	fill: none;
	stroke: currentColor;
	stroke-width: 1.5;
	stroke-linecap: round;
	stroke-linejoin: round;
	pointer-events: none;
}

.stp-capture__arc[hidden] {
	display: none;
}

.stp-capture__arc-row {
	display: flex;
	align-items: center;
	gap: var( --stp-gap-sm );
}

.stp-capture__range {
	flex: 1 1 auto;
	min-width: 0;
	/* The native control: it is keyboard-operable, announces its own value and
	 * is the one slider a screen-reader user already knows. */
	height: 32px;
	accent-color: var( --stp-accent );
}

/*
 * A number, not a sentence. Tabular figures so the row does not jitter as the
 * slider passes 9 to 10 and 99 to 100.
 */
.stp-capture__arc-value {
	flex: 0 0 auto;
	min-width: 3.5ch;
	font-size: 0.875em;
	font-variant-numeric: tabular-nums;
	text-align: right;
	color: var( --stp-ink-muted );
}

/* -------------------------------------------------------------------------
 * Fields
 * ---------------------------------------------------------------------- */

.stp-capture__fields {
	display: flex;
	flex-direction: column;
	gap: var( --stp-gap );
}

.stp-capture__field {
	display: flex;
	flex-direction: column;
	gap: 6px;
}

.stp-capture__label {
	font-size: 0.8125em;
	font-weight: 600;
	letter-spacing: 0.02em;
	text-transform: uppercase;
	color: var( --stp-ink-muted );
}

.stp-capture__input,
.stp-capture__select,
.stp-capture__textarea {
	width: 100%;
	padding: 10px 12px;
	font: inherit;
	/*
	 * THE iOS ZOOM FLOOR, stated in pixels on purpose. Same rule the notice
	 * button carries: iOS Safari zooms the whole page when a form control
	 * under 16px takes focus, and it does not zoom back out. `font: inherit`
	 * alone is not enough here — these controls sit inside panels whose base
	 * is 0.875em, so the select and the textarea measured 14px and 13.3px on
	 * staging while the text input beside them measured 16px purely because
	 * its own container happened to be full size. A customer picking a size
	 * or typing a note got the page thrown out of scale; the one typing their
	 * text did not. Not `1em` (that re-inherits the 14px) and not `1rem` (the
	 * theme owns the root); the card's own base is 16px and this pins to it.
	 */
	font-size: 16px;
	color: var( --stp-ink );
	background: var( --stp-surface );
	border: 1px solid var( --stp-border-strong );
	border-radius: var( --stp-radius-sm );
}

/*
 * The same floor, out-ranking the host page instead of overriding it. The
 * class rule above is (0,1,0), and WooCommerce core's
 * `woocommerce-blocktheme.css` sets `.woocommerce-page select { font-size:
 * var(--wp--preset--font-size--small) }` at (0,1,1) — on staging
 * (Twenty Twenty-Five) the size selects measured 14px at 390px while every
 * other field held 16px, and a select under 16px zooms the whole page on
 * iOS. Each selector here is at least (0,1,1) and the class pairs are
 * (0,2,0), which beats that rule on specificity alone, without depending on
 * stylesheet order and without `!important`. A theme that styles form
 * controls above (0,2,0) can still win; MB-03 on staging is the check.
 */
.stp-capture .stp-capture__input,
.stp-capture .stp-capture__select,
.stp-capture .stp-capture__textarea,
.stp-capture-launcher .stp-capture__input,
.stp-capture-launcher .stp-capture__select,
.stp-capture-launcher .stp-capture__textarea,
.stp-capture select,
.stp-capture textarea,
.stp-capture input:not([type="hidden"]):not([type="file"]):not([type="range"]):not([type="checkbox"]):not([type="radio"]),
.stp-capture-launcher select,
.stp-capture-launcher textarea,
.stp-capture-launcher input:not([type="hidden"]):not([type="file"]):not([type="range"]):not([type="checkbox"]):not([type="radio"]) {
	font-size: 16px;
}

.stp-capture__textarea {
	resize: vertical;
}

.stp-capture__preview {
	min-height: 64px;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 10px;
	font-size: 26px;
	line-height: 1.2;
	text-align: center;
	word-break: break-word;
	background: var( --stp-surface-alt );
	border: 1px dashed var( --stp-border );
	border-radius: var( --stp-radius-sm );
}

/*
 * The empty-preview placeholder is TEXT the customer reads ("Your text"), not a
 * border, so it takes the muted-ink token. --stp-border-strong is a line color;
 * used as type it fell below 4.5:1 on the panel background.
 */
.stp-capture__preview.is-empty {
	color: var( --stp-ink-muted );
}

/*
 * `font: inherit` DOES NOT CARRY text-transform OR letter-spacing — neither is
 * part of the `font` shorthand, so both survive the reset and come through from
 * whatever the theme puts on `button`. A great many of them uppercase and track
 * out every button on the page, which turned this into "START OVER" and, in
 * 0.10.0, the warning strip into "2 THINGS TO CHECK" — a sentence shouted at a
 * customer about an advisory that is deliberately not an error. Stated on every
 * button in the card that is meant to read as text.
 */
.stp-capture__link {
	align-self: flex-start;
	padding: 0;
	font: inherit;
	font-size: 0.875em;
	text-transform: none;
	letter-spacing: normal;
	color: var( --stp-accent-ink );
	background: none;
	border: 0;
	text-decoration: underline;
	cursor: pointer;
}

.stp-capture__link[hidden] {
	display: none;
}

/* -------------------------------------------------------------------------
 * Swatches
 * ---------------------------------------------------------------------- */

.stp-capture__swatches {
	display: grid;
	grid-template-columns: repeat( auto-fill, minmax( var( --stp-swatch-size ), 1fr ) );
	gap: 6px;
}

/*
 * PHONE SWATCHES ARE FINGER-SIZED. 34px was a mouse's target on a chart of 141
 * threads, and picking a color was the other half of the real-device
 * complaint ("hard to use the options to change colors and all that as its soo
 * small"). 44px is the ship-gate target, and with the resizable sheet there is
 * finally room to show a useful number of them at that size.
 */
@media ( max-width: 719.98px ) {

	.stp-capture {
		--stp-swatch-size: 44px;
	}
}

.stp-capture__swatch {
	position: relative;
	width: 100%;
	aspect-ratio: 1 / 1;
	min-height: var( --stp-swatch-size );
	padding: 0;
	background-clip: padding-box;
	border: 1px solid var( --stp-dark-25 );
	border-radius: var( --stp-radius-sm );
	cursor: pointer;
}

.stp-capture__swatch.is-on {
	box-shadow: 0 0 0 2px var( --stp-surface ), 0 0 0 4px var( --stp-accent );
}

/* A tick that reads on both light and dark swatches without relying on color
 * alone — selection must not be conveyed by hue only. */
.stp-capture__swatch.is-on::after {
	content: "";
	position: absolute;
	left: 50%;
	top: 46%;
	width: 30%;
	height: 55%;
	transform: translate( -50%, -50% ) rotate( 45deg );
	border: solid var( --stp-surface );
	border-width: 0 3px 3px 0;
	filter: drop-shadow( 0 0 1px var( --stp-dark-80 ) );
}

.stp-capture__chosen {
	margin: 0;
	color: var( --stp-ink-muted );
	font-size: 0.8125em;
}

/* -------------------------------------------------------------------------
 * Upload
 * ---------------------------------------------------------------------- */

.stp-capture__drop {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var( --stp-gap-sm );
	padding: var( --stp-gap );
	text-align: center;
	background: var( --stp-surface-alt );
	border: 2px dashed var( --stp-border-strong );
	border-radius: var( --stp-radius-sm );
}

.stp-capture__drop.is-over {
	border-color: var( --stp-accent );
	background: var( --stp-accent-tint-06 );
}

.stp-capture__drop-button {
	padding: 10px 16px;
	font: inherit;
	color: var( --stp-accent-contrast );
	background: var( --stp-accent );
	border: 0;
	border-radius: var( --stp-radius-sm );
	cursor: pointer;
}

.stp-capture__drop-button[disabled] {
	opacity: 0.6;
	cursor: default;
}

.stp-capture__file {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect( 0, 0, 0, 0 );
	white-space: nowrap;
	border: 0;
}

.stp-capture__progress {
	width: 100%;
	height: 6px;
}

.stp-capture__thumb {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: var( --stp-gap-sm );
}

.stp-capture__thumb:empty {
	display: none;
}

.stp-capture__thumb-image {
	width: 56px;
	height: 56px;
	object-fit: contain;
	background: var( --stp-surface-alt );
	border: 1px solid var( --stp-border );
	border-radius: var( --stp-radius-sm );
}

.stp-capture__chip {
	padding: 4px 10px;
	font-size: 0.8125em;
	word-break: break-all;
	background: var( --stp-surface-alt );
	border: 1px solid var( --stp-border );
	border-radius: 999px;
}

/* -------------------------------------------------------------------------
 * Footer
 * ---------------------------------------------------------------------- */

.stp-capture__footer {
	display: flex;
	flex-direction: column;
	gap: var( --stp-gap );
	padding: var( --stp-gap );
	background: var( --stp-surface-alt );
	border-top: 1px solid var( --stp-border );
}

.stp-capture__checkbox {
	display: flex;
	align-items: flex-start;
	gap: var( --stp-gap-sm );
	font-size: 0.9375em;
	cursor: pointer;
}

.stp-capture__checkbox input {
	margin-top: 3px;
}

/* -------------------------------------------------------------------------
 * THE WARNING STRIP — footer row 1.
 *
 * "2 things to check", above the action bar, tappable, and it opens the same
 * facts sheet the price disclosure opens.
 *
 * AMBER AND NEVER RED. Nothing behind this strip can block Add to cart: these
 * are advisories about how a design will PRODUCE, the shop confirms every job,
 * and a customer who wants 0.15" text after being told it will not stitch is a
 * conversation rather than an error. Painted in --stp-danger it would read as
 * "your order is broken" and cost the sale it exists to improve.
 *
 * 44px minimum, like every other tap target in the card, and 1em type so iOS
 * does not zoom the page when it is focused.
 * ---------------------------------------------------------------------- */

.stp-capture__warnstrip {
	display: flex;
	align-items: center;
	gap: var( --stp-gap-sm );
	width: 100%;
	min-height: 44px;
	margin: 0;
	padding: 8px 12px;
	font: inherit;
	/*
	 * FULL SIZE, not the 0.9375em the card's secondary text uses. Two reasons,
	 * and the second is the binding one: this is the only advisory a customer
	 * sees without opening anything, and iOS Safari zooms the whole page when a
	 * control under 16px is focused — measured here at 15px before this line,
	 * against a 16px card base.
	 */
	font-size: 1em;
	/*
	 * Neither is in the `font` shorthand, so `font: inherit` leaves both at
	 * whatever the theme put on `button` — and an uppercasing theme turned this
	 * into "2 THINGS TO CHECK", which reads as an alarm about something that is
	 * expressly not one. Same reset as .stp-capture__link.
	 */
	text-transform: none;
	letter-spacing: normal;
	text-align: left;
	color: var( --stp-notice );
	background: var( --stp-notice-bg );
	border: 0;
	border-left: 3px solid var( --stp-notice-edge );
	border-radius: var( --stp-radius-sm );
	cursor: pointer;
	/*
	 * The card's own box-sizing rule covers this too, but the strip is
	 * `width: 100%` with 24px of horizontal padding — the exact shape that
	 * overflowed its container on Twenty Twenty-Five before 0.9.1 fixed the
	 * class of bug. Stated rather than inherited.
	 */
	box-sizing: border-box;
}

.stp-capture__warnstrip[hidden] {
	display: none;
}

.stp-capture__warnstrip:hover,
.stp-capture__warnstrip:focus-visible {
	background: var( --stp-notice-14 );
}

.stp-capture__warnstrip:focus-visible {
	outline: none;
	box-shadow: var( --stp-focus );
}

/*
 * The mark is a "!" in the markup rather than an ::after glyph, so it survives
 * a copy-paste and is not invented by the stylesheet. Circled, at a fixed size,
 * so a two-line label does not stretch it into an oval.
 */
.stp-capture__warnstrip-mark {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 20px;
	height: 20px;
	font-size: 0.8125em;
	font-weight: 700;
	line-height: 1;
	color: var( --stp-notice );
	border: 2px solid currentColor;
	border-radius: 50%;
}

.stp-capture__warnstrip-label {
	flex: 1 1 auto;
	min-width: 0;
	font-weight: 600;
}

/*
 * The chevron. A CSS shape rather than a character, because ">" in the markup
 * is read aloud by a screen reader and "2 things to check greater than" is not
 * a thing anyone should hear.
 */
.stp-capture__warnstrip-chevron,
.stp-capture__price-chevron {
	flex: 0 0 auto;
	width: 8px;
	height: 8px;
	border-right: 2px solid currentColor;
	border-bottom: 2px solid currentColor;
	transform: rotate( -45deg );
}

.stp-capture__warnstrip-chevron {
	/* Points right: this one navigates INTO a sheet rather than expanding. */
	transform: rotate( -45deg );
	margin-right: 2px;
}

.stp-capture__price {
	display: flex;
	flex-direction: column;
	gap: 2px;
}

.stp-capture__price[hidden] {
	display: none;
}

.stp-capture__price-value {
	font-size: 1.0625em;
}

/* -------------------------------------------------------------------------
 * THE PRICE AS A DISCLOSURE
 *
 * A <button> only where there is a facts sheet behind it; with estimates off
 * the price is still the plain <strong> above and none of this applies.
 *
 * It must not LOOK like a button — it is the price, and turning the headline
 * figure of the action bar into a second call to action competes with Add to
 * cart. So: the same weight and size it had, plus a chevron, plus the ordinary
 * affordances of something pressable.
 * ---------------------------------------------------------------------- */

.stp-capture__price-value--button {
	display: inline-flex;
	align-items: center;
	gap: var( --stp-gap-sm );
	/*
	 * Self-start, or the button stretches to the footer column's full width and
	 * its 44px hit area becomes a 44px-tall band the customer taps by accident
	 * on the way to the quantity stepper.
	 */
	align-self: flex-start;
	min-height: 44px;
	margin: 0;
	padding: 0;
	font: inherit;
	font-size: 1.0625em;
	font-weight: 700;
	/*
	 * Outside the `font` shorthand, so `font: inherit` does not reach them and
	 * an uppercasing theme would set the price in tracked-out capitals. It is
	 * the price: it has to look like the number, not like a call to action.
	 * Same reset as the warning strip and .stp-capture__link.
	 */
	text-transform: none;
	letter-spacing: normal;
	text-align: left;
	color: inherit;
	background: none;
	border: 0;
	cursor: pointer;
}

.stp-capture__price-value--button:focus-visible {
	outline: none;
	box-shadow: var( --stp-focus );
	border-radius: var( --stp-radius-sm );
}

.stp-capture__price-number {
	min-width: 0;
}

/* Down when closed, up when open — the standing convention for a disclosure. */
.stp-capture__price-chevron {
	transform: rotate( 45deg );
	margin-top: -4px;
}

.stp-capture__price-value--button[aria-expanded="true"] .stp-capture__price-chevron {
	transform: rotate( -135deg );
	margin-top: 4px;
}

/*
 * The setup fee is its own line and never folded into the per-unit figure: it
 * is charged once per job across the whole cart and may be waived.
 */
.stp-capture__price-setup {
	font-size: 0.875em;
}

.stp-capture__price-setup[hidden] {
	display: none;
}

/*
 * The price's scope line. A sibling of the price block since 0.9.0 (it is
 * footnote text, and the action bar is for the number and the button), so it
 * carries its own margin reset — it is a <p> in someone else's theme now.
 */
.stp-capture__price-note {
	margin: 0;
	color: var( --stp-ink-muted );
	font-size: 0.75em;
	line-height: 1.4;
	text-align: center;
}

/* -------------------------------------------------------------------------
 * THE QUICK-CAPTURE FOOTER ORDER MAP — the studio row map's counterpart.
 *
 * Making the note a footer child was done for the STUDIO grid, which then
 * places it deliberately. Quick capture got the same node with no instructions:
 * this footer is a `flex-direction: column`, so "estimate only — taxes and
 * shipping extra" was rendered in DOM position, i.e. AFTER Add to cart, the
 * proof checkbox and the note field, with nothing but distance connecting it to
 * the number it qualifies (9th review round, MINOR 7 — a customer-visible
 * defect on the quick-capture surface, not a cosmetic one). The note goes back
 * under the price, where a footnote on a price belongs.
 *
 * This is the ONE place in the card where visual order runs ahead of DOM order,
 * and the trade is deliberate: the note is a footnote, so a screen reader
 * meeting it in the epilogue with the rest of the fine print reads correctly
 * either way, while an eye needs it next to the number to know what it is
 * about. Everything else keeps its DOM position — `order: 4` on the rest is
 * what makes that true, since equal `order` values fall back to DOM order.
 *
 * THE FACTS REGION TAKES THE SLOT BETWEEN THEM (0.10.0). It is the disclosure
 * the price button opens, so it belongs under the number and above the
 * footnote — which is also where it sits in the DOM, so this line only has to
 * keep the default from throwing it to the back with everything else. It is
 * `display: none` while closed and therefore costs nothing at rest.
 *
 * Studio is excluded rather than merely unaffected: its footer is a grid with
 * every child explicitly placed, where `order` would be inert but misleading.
 * ---------------------------------------------------------------------- */

.stp-capture:not( .is-studio ) .stp-capture__footer > * {
	order: 4;
}

/*
 * The warning strip leads, in both orders. It is already first in the DOM, so
 * this only has to stop the `order: 4` above from sending it to the back with
 * everything else — which it would, since the three rules below pull the price,
 * its disclosure and its footnote in FRONT of the default.
 */
.stp-capture:not( .is-studio ) .stp-capture__footer > .stp-capture__warnstrip {
	order: 0;
}

.stp-capture:not( .is-studio ) .stp-capture__footer > .stp-capture__price {
	order: 1;
}

.stp-capture:not( .is-studio ) .stp-capture__footer > .stp-capture__facts-region {
	order: 2;
}

.stp-capture:not( .is-studio ) .stp-capture__footer > .stp-capture__price-note {
	order: 3;
}

/*
 * The trust strip: the counterweight to the fidelity disclaimer, sitting with
 * the primary action because that is the moment the reassurance is for.
 */
.stp-capture__trust {
	margin: 6px 0 0;
	color: var( --stp-ink-muted );
	font-size: 0.75em;
	line-height: 1.4;
	text-align: center;
}

.stp-capture__trust[hidden] {
	display: none;
}

/*
 * The card's quantity stepper (inline presentation only — revealed by
 * mountInline when it finds a native input to mirror).
 */
.stp-capture__field--qty {
	display: flex;
	align-items: center;
	gap: var( --stp-gap-sm );
}

.stp-capture__field--qty[hidden] {
	display: none;
}

.stp-capture__qty {
	width: 5.5em;
	text-align: center;
}

.stp-capture__submit {
	width: 100%;
	padding: 14px 20px;
	font: inherit;
	font-weight: 600;
	color: var( --stp-accent-contrast );
	background: var( --stp-accent );
	border: 0;
	border-radius: var( --stp-radius-sm );
	cursor: pointer;
}

.stp-capture__submit[disabled] {
	opacity: 0.65;
	cursor: default;
}

/*
 * The way out of a resumed design. Deliberately a quiet text button in the
 * muted ink and deliberately NOT next to the primary one on a phone: it throws
 * work away, so it must not be what a thumb lands on by accident.
 */
.stp-capture__startover {
	align-self: center;
	/*
	 * ...but quiet is not the same as small. It measured 62x20 on a phone,
	 * under both the 24px floor in ARCHITECTURE.md and WCAG 2.5.8 AA, which is
	 * the worst of both worlds for a destructive control: hard to hit on
	 * purpose AND hard to hit accurately, so a miss lands on it exactly when
	 * the customer meant something else. The padding grows the target without
	 * raising the visual weight one bit — the ink, the size and the position
	 * are all unchanged, so the note above still holds.
	 */
	min-height: 24px;
	padding: 6px 12px;
	color: var( --stp-ink-muted );
}

.stp-capture__startover:hover {
	color: var( --stp-danger );
}

/* -------------------------------------------------------------------------
 * Studio layout — VISUAL MODE ONLY
 *
 * Canvas first: the garment across the top of the modal, an icon rail under it,
 * and one panel at a time coming up over the lower half of the stage. Quick
 * capture never sees any of this — every rule below is behind
 * `.stp-capture.is-studio`, and the rail and the sheet carry `hidden` there.
 *
 * The body becomes a two-area grid rather than a column of boxes, and the sheet
 * is a grid item placed in the STAGE area, aligned to its end. That is what
 * makes the panel an overlay with no absolute positioning to maintain and no
 * measurement of the rail's height: the rail keeps its own row and stays
 * reachable, the sheet sits over the canvas, and the canvas box does not change
 * size when a panel opens. It must not — a text item's font-size is fitted in
 * PIXELS against the measured canvas (fitLine), so a canvas that reflowed under
 * an opening panel would leave every fitted size describing a stage that is no
 * longer there.
 * ---------------------------------------------------------------------- */

.stp-capture.is-studio .stp-capture__body {
	display: grid;
	grid-template-columns: 1fr;
	grid-template-rows: 1fr auto;
	grid-template-areas:
		"stage"
		"rail";
	gap: 0;
	padding: 0;
	min-height: 0;
	/*
	 * STRETCH, explicitly. The 720px flex layout above sets align-items:
	 * flex-start for the two-column modal, and a grid inherits it — which left
	 * the tool bar sized to its own five buttons and a band of stage showing
	 * beneath it where the dark column should have run the full height. The
	 * chrome only reads as a frame if it reaches the edges.
	 */
	align-items: stretch;
	/*
	 * The BODY does not scroll here; the stage inside it does. An overlay
	 * anchored to a scrolling box scrolls away from the rail it is supposed to
	 * sit on top of.
	 *
	 * `clip` rather than `hidden`, and the PAIR is load-bearing. `hidden` still
	 * makes this a SCROLL CONTAINER - it only refuses the user a scrollbar, so
	 * the browser remains free to scroll it programmatically. The sheet is a
	 * grid item that spends the first frames of its open at translateY( 100% ),
	 * a transform contributes to scrollable overflow, and so any focus() that
	 * lands inside the sheet gets "revealed" by scrolling this box: measured
	 * 317px on a rail open and 361px entering the zone editor, which is the
	 * blank panel Aramis photographed on 0.9.0 (iPhone, 2026-08-20). `clip`
	 * clips WITHOUT creating a scroll container, so there is nothing left to
	 * scroll and the whole class of bug is gone rather than patched one caller
	 * at a time.
	 *
	 * `hidden` stays as the first declaration for Safari below 16, which drops
	 * the `clip` line; those browsers are covered behaviorally instead, by the
	 * preventScroll + assertion in focusInCard().
	 */
	overflow: hidden;
	overflow: clip;
}

.stp-capture.is-studio .stp-capture__stage {
	grid-area: stage;
	/* The 720px layout makes this sticky for the two-column view; a grid item
	 * in the studio is placed by the grid and nothing else. */
	position: static;
	/*
	 * LOAD-BEARING. Do not remove this line; it is not a leftover.
	 *
	 * The stage and the sheet are two items of the SAME grid. The sheet carries
	 * `z-index: 20` so a panel paints over the canvas it overlays. Inside the
	 * stage, the selected decoration is lifted with `z-index: 30`
	 * (.stp-capture__zone-items.is-lifted) so the thing being dragged is the
	 * thing you can see and grab. With no stacking context on the stage those
	 * two numbers are compared against each other in the body's context — 30
	 * beats 20 — and the lifted decoration paints straight THROUGH an open
	 * panel, on top of the very fields that are editing it.
	 *
	 * `z-index` applies to a grid item even at `position: static`, and any value
	 * other than `auto` makes it a stacking context. That is what makes one
	 * declaration enough: the lift still wins against its siblings inside the
	 * stage, and the stage as a whole now loses to the sheet, always. `0` is
	 * chosen because it changes nothing about where the stage itself paints.
	 */
	z-index: 0;
	min-width: 0;
	min-height: 0;
	overflow-y: auto;
	-webkit-overflow-scrolling: touch;
	padding: var( --stp-gap-sm ) var( --stp-gap-sm ) var( --stp-gap );
	background: var( --stp-canvas-bg );
}

/*
 * THE CANVAS BOX IS THE IMAGE BOX, and stays that way.
 *
 * Zone rects and every stored placement are percentages of the view image
 * (SPEC-SCHEMA), and both the hotspot layer and the item layer are inset:0 of
 * this element — so giving the canvas a height of its own and letterboxing the
 * garment inside it would put dead bars inside the coordinate frame and move
 * every zone. Instead the IMAGE is capped and the canvas shrink-wraps it, which
 * is the only way to get a ~55vh stage without touching the geometry.
 */
.stp-capture.is-studio .stp-capture__canvas {
	display: block;
	width: -webkit-fit-content;
	width: fit-content;
	max-width: 100%;
	margin: 0 auto;
	background: none;
	border: 0;
	border-radius: 0;
}

.stp-capture.is-studio .stp-capture__image {
	width: auto;
	height: auto;
	max-width: 100%;
	max-height: var( --stp-stage-h, 55vh );
}

.stp-capture.is-studio .stp-capture__views {
	justify-content: center;
}

/*
 * The mode pills sit between the dark top bar and the canvas. Centered and
 * tightened so they read as a small switch under the chrome rather than as a
 * second row of navigation.
 */
.stp-capture.is-studio .stp-capture__capture-modes {
	justify-content: center;
	padding: var( --stp-gap-sm ) var( --stp-gap-sm ) 0;
}

.stp-capture.is-studio .stp-capture__disclaimer {
	text-align: center;
}

/*
 * The stage instruction goes SR-ONLY in the studio layout, and stays in the DOM.
 *
 * It is the target of every stage item's aria-describedby — the keyboard
 * contract for moving a decoration is described by this string and nowhere else
 * — so it cannot be removed or `hidden`. What it can stop being is a line of
 * help text printed under the garment: the card's whole claim is that it looks
 * like a product, and the same sentence is one press of Info away for anyone who
 * wants it. Quick capture keeps it visible; it has no Info button and its zone
 * outlines are its interface.
 */
.stp-capture.is-studio .stp-capture__hint {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect( 0, 0, 0, 0 );
	white-space: nowrap;
	border: 0;
}

/*
 * SHORT VIEWPORTS — a phone in landscape, a small laptop, a browser with three
 * toolbars open.
 *
 * The stage was tuned against a portrait phone: 55vh is the garment before the
 * modal's own header, the mode pills, the rail and the footer have taken
 * anything. Under the height below that collapses — the garment is squeezed to
 * a strip. The stage comes down, which leaves the fixed chrome the room it
 * needs.
 *
 * WHERE 760 COMES FROM, and why it is not 620 any more. The inline card sizes
 * its stage as `max( 260px, 100svh - --stp-chrome-h )`, so the FLOOR takes over
 * — and the chrome starts being squeezed instead of the garment — as soon as
 * the viewport drops under `chrome + 260`. The chrome measured at 430x932 was
 * ~440 (top bar 52, mode pills 38, rail 66, action-bar footer ~282, borders),
 * which put that crossover at ~700 and made 620 an under-estimate even before
 * this wave. 0.10.0 then adds a row to the action bar: the warning strip is
 * 44px of min-height plus one 8px row-gap, ~52px, call the chrome ~500. 500 +
 * 260 = 760, and every viewport from there down is one where the floor is
 * holding the garment up while the chrome overflows. 621–760px — the iPhone SE
 * class, and a phone in landscape with a toolbar open — was the band that fell
 * between the old number and the real one.
 *
 * The interior values are unchanged: 34vh was tuned against the layout, not
 * against the breakpoint, and it stays the stage this block asks for.
 *
 * The SHEET is not in here any more: it is a percentage of the stage area and
 * the customer sizes it themselves (--stp-sheet-size, written by the grip), so
 * it follows the stage down without a second number to keep in step.
 *
 * A custom property rather than a new rule, so this is the same knob a shop
 * already overrides, set to a different number at a different height.
 *
 * TWO SELECTORS, and that is not redundancy. A custom property is resolved by
 * INHERITANCE — nearest declaration wins, and specificity only breaks ties
 * between declarations on the SAME element. `.stp-capture` is the overlay/embed
 * root; the inline card declares --stp-stage-h on `.stp-capture__dialog
 * .is-inline`, which is its descendant, so from 0.9.0 until this line the whole
 * media query was dead on the primary surface no matter how it was written
 * (9th review round, MAJOR 1: measured at 430x600, the dialog was still
 * resolving max(260px, 100svh - chrome) while the root said 34vh). The second
 * rule is the same knob declared where the card can actually see it, at equal
 * specificity and later in source than the block at the top of this file.
 */
@media ( max-height: 760px ) {

	.stp-capture {
		--stp-stage-h: 34vh;
	}

	.stp-capture__dialog.is-inline {
		--stp-stage-h: 34vh;
	}
}

/*
 * The accordion column, the note field and the proof choice are LENT to their
 * panels — the same nodes, re-parented, never a second copy. All three rules use
 * a child combinator on purpose: once the node is inside the sheet its parent is
 * different and the rule stops applying to it, with no class to add and remove
 * in step with the move.
 */
.stp-capture.is-studio .stp-capture__body > .stp-capture__panel {
	display: none;
}

.stp-capture.is-studio .stp-capture__footer > .stp-capture__field--note {
	display: none;
}

/*
 * ...AND THE PROOF CHOICE JOINS IT (Wave 1).
 *
 * The studio footer is an ACTION BAR: the price, the quantity, Add to cart, and
 * the standing fine print under them. "Email me a proof before production" is
 * not fine print and it is not an action — it is a DECISION, and a decision
 * belongs with the other decisions, in the review step, next to the note field
 * that has been there since the studio layout shipped (openStudioPanel lends
 * both). Left in the footer it sat on row 3, below the fold, where a customer
 * met it after they had already been offered the button.
 *
 * Same technique, same reason, same consequence: the child-combinator scopes
 * this to the footer, so the field is fully visible the moment the review panel
 * takes it, and — see the row map below — a hidden child needs no row.
 */
.stp-capture.is-studio .stp-capture__footer > .stp-capture__field--proof {
	display: none;
}

/*
 * The fidelity disclaimer, moved into the footer for the length of visual mode
 * (studioDisclaimer in capture.js — the same node, re-parented, exactly as the
 * note field is).
 *
 * It says the stage is an approximation drawn from the customer's own screen
 * rather than the artwork the shop will run, and SPEC-SCHEMA calls it permanent
 * while visual mode is on. On the stage it was not: the lower half of the stage
 * is where a panel comes up, so the first sheet a customer opened covered the
 * sentence and it stayed covered. The footer is outside .stp-capture__body,
 * which is the one region a sheet can never reach.
 *
 * Its ROW is assigned with the rest of them in the row map below — this rule
 * only carries what is true wherever it sits. The row lived here once and
 * collided with the price when the price moved onto row 3 (0.8.1), because a
 * child-combinator selector quietly outranked the map; one place owns the
 * rows now.
 */
.stp-capture.is-studio .stp-capture__footer > .stp-capture__disclaimer {
	grid-column: 1 / -1;
	margin: 0;
}

/*
 * A shorter footer, because the studio spends the modal's height on the garment.
 * Higher specificity than the 720px grid below it, so it holds at every width.
 */
.stp-capture.is-studio .stp-capture__footer {
	display: grid;
	grid-template-columns: 1fr auto;
	align-items: center;
	gap: var( --stp-gap-sm ) var( --stp-gap );
	padding: 10px var( --stp-gap );
}

/* -------------------------------------------------------------------------
 * THE FOOTER ROW MAP — one place, every row, no exceptions.
 *
 * Rows 1–2 are the ACTION BAR: the estimate over the quantity on the left,
 * Add to cart spanning both beside them. That is the whole of the footer that
 * has to be on screen with the garment, and the card is sized so the fold
 * falls under it (see --stp-stage-h on the inline card).
 *
 * Rows 3–6 are the epilogue, met by scrolling: the price's scope line, the
 * fidelity caveat, the trust line, and — last, where a thumb reaching for the
 * button cannot find it — Start over.
 *
 * THE PROOF CHOICE IS NOT IN THIS MAP ANY MORE (Wave 1) and every row below
 * where it used to sit has moved up by one. It is `display: none` in this
 * footer, on loan to the review step with the note field (see the rule above
 * this block), and a hidden child needs no row — the same answer, for the same
 * reason, that the note field has had since 0.9.1. Everything from the price's
 * scope line down is one row earlier in BOTH blocks; if it is put back, it
 * takes row 3 and they all move down again.
 *
 * THE WARNING STRIP (0.10.0) SITS ABOVE ALL OF IT, in a second copy of this
 * map that takes effect only while the strip is on screen. It is a footer
 * child like any other and it has its lines; they are in the `has-warn` block
 * below, with the reason it could not simply be numbered row 1 here.
 *
 * THE FACTS REGION IS A FOOTER CHILD AND IT IS PARKED LAST. It used to be
 * built inside `.stp-capture__price` precisely so this grid would never see
 * it — it expands to several hundred pixels, and an action bar is the last
 * place that should happen. That parenting was a bug: the price block is
 * hidden whenever there is no estimate to show, and the warning strip stays up
 * through that state, so the region the strip opened was `display: none` and
 * `aria-expanded` lied about it. It is a sibling now, and it gets the LAST row
 * in both blocks below.
 *
 * Last, for two reasons that hold together. On this surface the region is
 * never open at all — syncFactsControls() closes it on the way into the studio
 * and the sheet takes over — so the row is a guarantee rather than a layout.
 * And a trailing row assigned to a `display: none` child creates nothing,
 * which is what keeps the epilogue exactly the height it was; a row NUMBERED
 * in the middle would have created the phantom row this map has already paid
 * for once.
 *
 * The strip's live region (`.stp-capture__sr`) is the one footer child with no
 * line, and it needs none: it is absolutely positioned, so it is not a grid
 * item and cannot take a row.
 *
 * EVERY child gets a row here. Two rounds of bugs came out of leaving some to
 * auto-placement while others were pinned (the trust strip landing beside the
 * checkbox; the disclaimer sharing row 3 with the price and painting over it),
 * and both times the selector that actually decided the row was somewhere
 * else in this file. If a child is added to the footer, it gets a line here.
 *
 * THE ROW ORDER IS THE DOM ORDER. A grid can put a child anywhere, so the two
 * only agree because they are kept in step deliberately: a screen reader reads
 * the footer in DOM order and an eye reads it in row order, and a footer where
 * those disagree tells two different stories about which sentence belongs to
 * which control. If you add a row here, add the node in the same position in
 * buildModal() (capture.js).
 *
 * THERE IS NO ROW FOR THE NOTE FIELD OR THE PROOF CHOICE, and that is the fix
 * rather than an oversight. Both are `display: none` in the studio footer BY
 * DESIGN (they are on loan to the review step), so the rows they used to own
 * were permanently empty — and an empty grid row still costs its `row-gap`.
 * With the price hidden, the trust line untranslated and Start over absent as
 * well, that was up to ~32px of unexplained slack in a footer this wave exists
 * to shorten (9th review round, MINOR 10). A hidden child needs no row.
 * ---------------------------------------------------------------------- */

.stp-capture.is-studio .stp-capture__price {
	grid-column: 1;
	grid-row: 1;
}

.stp-capture.is-studio .stp-capture__field--qty {
	grid-column: 1;
	grid-row: 2;
}

.stp-capture.is-studio .stp-capture__submit {
	grid-column: 2;
	grid-row: 1 / span 2;
	align-self: center;
	width: auto;
	padding: 12px 20px;
}

.stp-capture.is-studio .stp-capture__footer > .stp-capture__price-note {
	grid-column: 1 / -1;
	grid-row: 3;
}

.stp-capture.is-studio .stp-capture__footer > .stp-capture__disclaimer {
	grid-row: 4;
}

.stp-capture.is-studio .stp-capture__trust {
	grid-column: 1 / -1;
	grid-row: 5;
}

.stp-capture.is-studio .stp-capture__startover {
	grid-column: 1 / -1;
	grid-row: 6;
	justify-self: center;
}

/* Never open on this surface — see the map's header for why it is last. */
.stp-capture.is-studio .stp-capture__footer > .stp-capture__facts-region {
	grid-column: 1 / -1;
	grid-row: 7;
}

/* -------------------------------------------------------------------------
 * ...AND THE SAME MAP WITH THE WARNING STRIP IN IT.
 *
 * The strip is hidden whenever the design has nothing to check, which is most
 * of the time, so it cannot simply be numbered row 1 with everything shifted
 * to 2–8: `grid-row: 2` on the price CREATES row 1 whether or not anything is
 * in it, and an empty grid row still costs its `row-gap`. That is precisely
 * the defect 0.9.1 removed from this footer (9th review round, MINOR 10 — a
 * permanently empty row eating ~8px of garment on a phone), and reintroducing
 * it here in exchange for one line of CSS would be a poor trade.
 *
 * So the map has TWO STATES and capture.js says which: `has-warn` goes on the
 * footer for exactly as long as the strip is visible (refreshFacts). The block
 * above is the resting layout, byte-for-byte what 0.9.1 shipped; this block is
 * the same rows shifted down by one with the strip on top.
 *
 * IF YOU ADD A FOOTER CHILD IT NEEDS A LINE IN BOTH BLOCKS. That is the price
 * of the two states, it is written here so nobody has to discover it, and the
 * alternative was a phantom row that this footer has already paid for once.
 * ---------------------------------------------------------------------- */

.stp-capture.is-studio .stp-capture__footer.has-warn .stp-capture__warnstrip {
	grid-column: 1 / -1;
	grid-row: 1;
}

.stp-capture.is-studio .stp-capture__footer.has-warn .stp-capture__price {
	grid-row: 2;
}

.stp-capture.is-studio .stp-capture__footer.has-warn .stp-capture__field--qty {
	grid-row: 3;
}

.stp-capture.is-studio .stp-capture__footer.has-warn .stp-capture__submit {
	grid-row: 2 / span 2;
}

.stp-capture.is-studio .stp-capture__footer.has-warn > .stp-capture__price-note {
	grid-row: 4;
}

.stp-capture.is-studio .stp-capture__footer.has-warn > .stp-capture__disclaimer {
	grid-row: 5;
}

.stp-capture.is-studio .stp-capture__footer.has-warn .stp-capture__trust {
	grid-row: 6;
}

.stp-capture.is-studio .stp-capture__footer.has-warn .stp-capture__startover {
	grid-row: 7;
}

.stp-capture.is-studio .stp-capture__footer.has-warn > .stp-capture__facts-region {
	grid-row: 8;
}

/* -------------------------------------------------------------------------
 * Studio: the icon rail
 * ---------------------------------------------------------------------- */

/*
 * The tool bar. Dark chrome, large icons, white labels — the frame around the
 * garment, matching the top bar above it.
 *
 * SCROLLS RATHER THAN SHRINKS. The buttons used to divide the width between
 * them (`flex: 1 1 0`), which meant a sixth tool, or a locale whose words are
 * longer, silently squeezed every label past the point of being readable. At a
 * fixed minimum they overflow instead, and the bar scrolls — the same answer
 * the reference toolbar gives, and the only one that keeps a label a label.
 *
 * ...and once it can overflow, `justify-content` has to be `flex-start`.
 * `space-around` distributes free space by putting half a share OUTSIDE the
 * first and last items — which is fine while everything fits and is a bug the
 * moment it does not: in an overflowing scroll container that leading half-share
 * becomes negative offset, and the first tool is unreachable in the scroll
 * direction. It cannot be scrolled to, because there is nothing before the
 * origin to scroll back to. The rail is packed from the start instead, so the
 * first tool is where a rail's first tool has always been and the overflow all
 * happens at the end, where the scroll can reach it.
 */
.stp-capture__rail {
	grid-area: rail;
	display: flex;
	align-items: stretch;
	justify-content: flex-start;
	gap: 2px;
	padding: 4px var( --stp-gap-sm );
	overflow-x: auto;
	scrollbar-width: thin;
	color: var( --stp-chrome-ink );
	background: var( --stp-chrome-bg );
	border-top: 1px solid var( --stp-light-12 );
}

/*
 * THE SCROLL AFFORDANCE. A row of tools that stops flush at the card's edge
 * looks like the whole set — nothing about it says a sixth one is a swipe away,
 * and on a phone with no visible scrollbar (every phone) the tools past the edge
 * were simply not discovered.
 *
 * A STICKY FLEX ITEM, which is what makes this self-hiding without a line of JS
 * or a wrapper element. The pseudo-element is the last item in the flex row, so
 * it takes part in the layout rather than covering it; `position: sticky` with
 * `right: 0` pins it to the right edge of the SCROLLPORT while the buttons pass
 * underneath. The gradient runs to the rail's own background color, so:
 *
 *   nothing to scroll  — it sits in flow at the end of the row, painting a fade
 *                        from transparent to the background ON the background.
 *                        Invisible, exactly as it should be.
 *   more to the right  — it is pinned at the edge with buttons under it, and
 *                        those buttons fade out into the card edge.
 *   scrolled to the end — its flow position IS the right edge, so it is back
 *                        over empty rail and disappears again.
 *
 * No negative margin, deliberately: it adds its own width to the row instead of
 * overlapping the last button, which is why the "nothing to scroll" case can be
 * genuinely invisible rather than a permanent smudge on the last label.
 */
.stp-capture__rail::after {
	content: "";
	position: sticky;
	right: 0;
	flex: 0 0 28px;
	align-self: stretch;
	/*
	 * The transparent end is the chrome color AT ZERO ALPHA, not the
	 * `transparent` keyword and not a white — either of those drags the ramp
	 * through a color the bar does not contain (transparent is transparent
	 * BLACK, and white would gray the middle), which reads as a dirty smear
	 * rather than a fade.
	 *
	 * Written out because a custom property cannot be given an alpha in a
	 * browser this file still supports (no color-mix, no relative color
	 * syntax). --stp-chrome-fade is --stp-chrome-bg (#1b2c46) at zero alpha; a
	 * skin that moves the chrome moves the fade with it.
	 */
	background: linear-gradient( to right, var( --stp-chrome-fade ), var( --stp-chrome-bg ) );
	pointer-events: none;
}

.stp-capture__rail[hidden] {
	display: none;
}

.stp-capture__rail-button {
	display: flex;
	/*
	 * GROW to share a wide rail, SHRINK no further than the 50px floor. At
	 * the old fixed 68px the five-tool set plus the 28px fade item overflowed
	 * the ~315px the inline card actually gives the rail on a 375px phone —
	 * the fifth tool sat half-clipped under the fade on the most common
	 * viewport there is, looking cut off rather than scrollable. At the 50px
	 * floor all five fit that rail (5×50 + 4×2 gap + 24 padding + 28 fade =
	 * 310 ≤ 315) and still clear the 44px target; a sixth tool overflows and
	 * the fade affordance takes over, which is what it is for. The narrow
	 * floor needs the two-line label clamp below, widened to cover every
	 * phone this floor can occur on.
	 */
	flex: 1 1 56px;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 4px;
	/* Mobile ship gate: a 44px target, reachable one-handed at the bottom of
	 * the modal. Wider than that so the (possibly two-line) label fits. */
	min-width: 50px;
	min-height: 56px;
	padding: 6px 6px;
	font: inherit;
	font-size: 11px;
	line-height: 1.15;
	text-align: center;
	color: var( --stp-light-86 );
	background: transparent;
	border: 1px solid transparent;
	border-radius: var( --stp-radius-lg );
	cursor: pointer;
}

.stp-capture__rail-button[hidden] {
	display: none;
}

.stp-capture__rail-button:hover {
	color: var( --stp-chrome-ink );
	background: var( --stp-light-08 );
}

/*
 * The open tool. A white wash rather than the accent: the accent belongs to the
 * customer's choices on the garment, and a rail button lit in the same color
 * as a selected swatch reads as another one of them.
 */
.stp-capture__rail-button.is-on {
	color: var( --stp-chrome-ink );
	background: var( --stp-light-14 );
	border-color: var( --stp-light-42 );
}

.stp-capture__rail-button :focus-visible,
.stp-capture__rail-button:focus-visible {
	outline: 2px solid var( --stp-chrome-ink );
	outline-offset: -2px;
	box-shadow: none;
}

/* Large enough to be read as a picture rather than a mark. */
.stp-capture__rail-icon {
	width: 26px;
	height: 26px;
}

/*
 * The label is visible AND is the accessible name. Five icons a customer meets
 * once are five guesses without it, and unlike the seven-wide parity row this
 * rail has the width for words.
 */
.stp-capture__rail-label {
	display: block;
	max-width: 100%;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/*
 * PHONES (and any locale whose words are longer than English's).
 *
 * With the 50px shrink floor above, five rail buttons in a ~315px inline-card
 * rail leave about 51px each, and one line of nowrap text in that space
 * ellipsises "Add artwork" down to "Add…" — a visible label that has stopped
 * being a label. On phone widths the label is allowed two lines instead,
 * clamped so a three-word translation cannot grow the rail into the canvas.
 * The ellipsis still lands, on the second line, where it costs a word rather
 * than the meaning. 719px matches the studio desktop breakpoint, where the
 * rail stands up into a column and the squeeze cannot happen.
 */
@media ( max-width: 719.98px ) {

	.stp-capture__rail-label {
		display: -webkit-box;
		-webkit-box-orient: vertical;
		-webkit-line-clamp: 2;
		line-clamp: 2;
		white-space: normal;
	}
}

/* -------------------------------------------------------------------------
 * Studio: the panel (bottom sheet on a phone, left drawer from 720px)
 * ---------------------------------------------------------------------- */

.stp-capture__sheet {
	/* The SAME grid area as the stage, aligned to its end — so it overlays the
	 * lower part of the canvas without taking the rail's row away from it. */
	grid-area: stage;
	align-self: end;
	z-index: 20;
	display: flex;
	flex-direction: column;
	width: 100%;
	/*
	 * THE CUSTOMER SETS THIS. capture.js writes --stp-sheet-size from the grip
	 * (drag, tap to cycle, arrow keys) and remembers the choice for the page.
	 *
	 * A LENGTH, not a percentage, and that is load-bearing. The sheet is a grid
	 * item in the stage's area, and a percentage height against an area whose
	 * own height is indefinite (this card is a block in a page, not a
	 * viewport-capped modal) is treated as `auto` while the row is being sized
	 * — so the row grew to the sheet's CONTENT, which for a 141-swatch thread
	 * chart is thousands of pixels. Measured on staging: the stage area went to
	 * 1129px around a 354px garment. A definite length contributes itself and
	 * nothing else.
	 *
	 * `svh` is the viewport with the phone's browser toolbars showing, which is
	 * the state the customer is actually in; the `vh` line before each is the
	 * fallback for browsers that do not know `svh`.
	 *
	 * The old flat cap (half the stage) was the complaint on a real phone: with
	 * the stage at 55vh the editor got about a quarter of the screen, which is
	 * not enough to work a thread chart in.
	 *
	 * ABOVE THE GARMENT'S HEIGHT THE STAGE ROW GROWS, and as of 0.9.1 that is
	 * deliberate. This is a grid item in a `1fr` row of a container with an
	 * indefinite block size, so the row is sized from its items' max-content
	 * contributions and a definite length contributes itself: a sheet taller
	 * than the canvas makes the row — and the card — taller, and shrinks them
	 * again on close. That is the only way this layout can produce the
	 * full-screen panel Aramis asked for without going `position: fixed`, which
	 * lands behind the iOS keyboard (see the 3.7 rejection in
	 * docs/reviews/2026-08-20-ios-sheet-diagnosis.md). The canvas does NOT move
	 * while it happens — the row grows by adding empty space below a
	 * `fit-content` canvas — and capture.js scrolls the grown card back into
	 * view on open (revealSheet).
	 */
	height: var( --stp-sheet-size, 58vh );
	height: var( --stp-sheet-size, 58svh );
	overflow: hidden;
	background: var( --stp-surface );
	border-radius: var( --stp-radius-lg ) var( --stp-radius-lg ) 0 0;
	box-shadow: 0 -6px 24px var( --stp-shade-16 );
	/*
	 * Hidden by VISIBILITY as well as by the attribute: the panel is unhidden a
	 * frame before it is told to slide, so that the transition has a starting
	 * state to run from, and without this that frame would show the closed
	 * sheet sitting over the rail.
	 */
	visibility: hidden;
	transform: translateY( 100% );
	transition:
		transform 0.24s ease-out,
		height 0.18s ease-out;
}

/*
 * A height being DRIVEN BY A FINGER is not animated: the panel has to sit under
 * the grip, not chase it. capture.js adds this for the length of the drag only.
 */
.stp-capture__sheet.is-resizing {
	transition: none;
}

.stp-capture__sheet[hidden] {
	display: none;
}

.stp-capture__sheet.is-open {
	visibility: visible;
	transform: translateY( 0 );
}

.stp-capture__sheet:focus {
	outline: none;
}

/* Affordance only — it says the panel came up from the bottom. The close button
 * and Escape are the two ways out, and both are real controls. */
/*
 * The grip: a real resize control, and sized like one.
 *
 * The BAR is 40×4; the TARGET is the full width of the sheet and 28px tall,
 * because this is the first thing a thumb reaches for when the panel is too
 * short and it used to be a 4px decal that did nothing. The bar itself is the
 * pseudo-element, so the target can be finger-sized without drawing a
 * finger-sized slab across the top of the editor.
 *
 * `touch-action: none` because the gesture IS the resize — without it the
 * browser takes the vertical drag for a page scroll before the first
 * pointermove arrives.
 */
.stp-capture__sheet-grip {
	position: relative;
	flex: 0 0 auto;
	width: 100%;
	height: 28px;
	touch-action: none;
	cursor: row-resize;
}

.stp-capture__sheet-grip::before {
	content: "";
	position: absolute;
	top: 10px;
	left: 50%;
	width: 40px;
	height: 4px;
	margin-left: -20px;
	background: var( --stp-border-strong );
	border-radius: 999px;
}

/* Bigger and darker while it is being used, so the bar reads as the thing the
 * finger has hold of. */
.stp-capture__sheet-grip:active::before,
.stp-capture__sheet-grip:focus-visible::before {
	width: 56px;
	margin-left: -28px;
	background: var( --stp-ink-muted );
}

.stp-capture__sheet-grip:focus-visible {
	outline: 2px solid var( --stp-accent );
	outline-offset: -4px;
	border-radius: var( --stp-radius-sm );
}

/*
 * The desktop drawer is full height already — there is no vertical size to
 * choose, and capture.js declines to write one there (isSheetResizable).
 */
@media ( min-width: 720px ) {

	.stp-capture__sheet-grip {
		display: none;
	}
}

.stp-capture__sheet-head {
	display: flex;
	flex: 0 0 auto;
	align-items: center;
	justify-content: space-between;
	gap: var( --stp-gap-sm );
	padding: 4px var( --stp-gap ) 8px;
	border-bottom: 1px solid var( --stp-border );
}

.stp-capture__sheet-title {
	margin: 0;
	overflow: hidden;
	font-size: 0.9375em;
	font-weight: 700;
	line-height: 1.2;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.stp-capture__sheet-close {
	flex: 0 0 auto;
	width: 36px;
	height: 36px;
	padding: 0;
	font-size: 22px;
	line-height: 1;
	color: var( --stp-ink-muted );
	background: transparent;
	border: 1px solid transparent;
	border-radius: var( --stp-radius-sm );
	cursor: pointer;
}

.stp-capture__sheet-close:hover {
	color: var( --stp-ink );
	border-color: var( --stp-border );
}

/* The panel scrolls INSIDE itself; the stage behind it never moves. */
.stp-capture__sheet-body {
	flex: 1 1 auto;
	min-height: 0;
	overflow-y: auto;
	-webkit-overflow-scrolling: touch;
	padding: var( --stp-gap );
}

.stp-capture__sheet-body > .stp-capture__panel {
	width: 100%;
}

/*
 * A zone's editor body arrives without the section that framed it, so the
 * padding and the rule that separated it from its own heading go with the
 * frame. The node itself is untouched.
 */
.stp-capture__sheet-body > .stp-capture__editor-body {
	padding: 0;
	border-top: 0;
}

/* -------------------------------------------------------------------------
 * Studio: list panels (Elements, and the "which area?" chooser)
 * ---------------------------------------------------------------------- */

.stp-capture__stack {
	display: flex;
	flex-direction: column;
	gap: var( --stp-gap-sm );
}

.stp-capture__empty {
	margin: 0;
	padding: var( --stp-gap ) 0;
	color: var( --stp-ink-muted );
	font-size: 0.875em;
	text-align: center;
}

.stp-capture__row {
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 4px 6px 4px 0;
	background: var( --stp-surface );
	border: 1px solid var( --stp-border );
	border-radius: var( --stp-radius-lg );
}

/* The row for the decoration the stage has selected — the same state the ring
 * on the garment is showing, said twice so the two surfaces agree. */
.stp-capture__row.is-on {
	border-color: var( --stp-accent );
}

.stp-capture__row-pick {
	display: flex;
	flex: 1 1 auto;
	align-items: center;
	gap: var( --stp-gap-sm );
	min-width: 0;
	min-height: 44px;
	padding: 6px 10px;
	font: inherit;
	text-align: left;
	color: var( --stp-ink );
	background: transparent;
	border: 0;
	border-radius: var( --stp-radius-lg );
	cursor: pointer;
}

.stp-capture__row-pick--zone {
	width: 100%;
	background: var( --stp-surface );
	border: 1px solid var( --stp-border );
}

/* At its cap and everything in it decorated. Still openable — editing what is
 * there is a reasonable thing to want — but not offered as if it were empty. */
.stp-capture__row-pick--zone.is-full {
	opacity: 0.72;
}

.stp-capture__row-icon {
	flex: 0 0 auto;
	width: 18px;
	height: 18px;
	color: var( --stp-ink-muted );
}

/* The area picker's thumbnail: the garment, with THIS area lit on it.
 *
 * TWO boxes, and the split is load-bearing.
 *
 * The TILE is fixed, so four rows put their four labels at the same x. Sizing
 * each tile to its own view's aspect (a cap front is 546x540, its back 926x716)
 * left the column ragged, which is the opposite of the point.
 *
 * The FIT is the image's own box, sized by JS (`sizeRowThumb`) to a contain-fit
 * inside the tile. The highlight lives in there and NOT in the tile, because
 * the zone rect is a percentage OF THE IMAGE: measured against a padded tile it
 * would land off the area, subtly and differently for every view.
 *
 * `object-fit: contain` cannot do this job — it fits the picture but leaves the
 * letterbox inside the element, so percentages still resolve against the padded
 * box. Nor can the CSS-only `height:100%; width:auto`: that is circular, and
 * browsers resolve it by contributing the image's NATURAL width to the
 * shrink-to-fit parent. Measured here before it was fixed: 546, 926 and 1000px
 * wide tiles inside a 44px row, the overflow merely hidden.
 *
 * 44px is the row's own min-height, so the picture rides inside the row the
 * customer already had rather than making the sheet taller. This list is the
 * first thing on a phone and its height is the whole reason it works there.
 *
 * ⚠ 64 and 44 are `ROW_THUMB_W` and `ROW_THUMB_H` in capture.js. They must
 * move together.
 */
.stp-capture__row-thumb {
	display: flex;
	flex: 0 0 auto;
	align-items: center;
	justify-content: center;
	width: 64px;
	height: 44px;
	overflow: hidden;
	background: var( --stp-surface-alt );
	border-radius: var( --stp-radius-sm );
}

/* Before the image resolves this has no size, and the tile's own background is
 * what shows: a tidy blank chip rather than a collapsed sliver. */
.stp-capture__row-thumb-fit {
	position: relative;
	display: block;
}

.stp-capture__row-thumb-img {
	display: block;
	width: 100%;
	height: 100%;
}

/* Lit, and ringed in the same white the stage rings its zones with.
 *
 * The stage draws a zone as `2px dashed var( --stp-zone-outline )`. At 44px a
 * dashed 2px border is mush, so the patch is filled with the accent and keeps
 * the WHITE EDGE — the part of that language that survives the size, and the
 * same token, so a shop that recolors its zone outline recolors this too.
 *
 * box-shadow rather than a border: the ring must not grow the patch. A 1px
 * border on each side is two pixels at a size where two pixels is a visible
 * lie about where the area sits on the garment.
 */
.stp-capture__row-thumb-zone {
	position: absolute;
	background: var( --stp-accent );
	box-shadow: 0 0 0 1px var( --stp-zone-outline );
	opacity: 0.55;
}

@media ( forced-colors: active ) {
	/* The wash is a color the mode drops. An outline is the one thing it keeps,
	 * and at that point the two-pixel growth is the lesser wrong. */
	.stp-capture__row-thumb-zone {
		background: transparent;
		border: 1px solid CanvasText;
		box-shadow: none;
		opacity: 1;
	}
}

.stp-capture__row-text {
	display: flex;
	flex: 1 1 auto;
	flex-direction: column;
	min-width: 0;
}

.stp-capture__row-zone {
	overflow: hidden;
	font-size: 0.875em;
	font-weight: 600;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.stp-capture__row-summary {
	overflow: hidden;
	color: var( --stp-ink-muted );
	font-size: 0.8125em;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.stp-capture__row-summary:empty {
	display: none;
}

/* Tabular figures so a column of layer numbers does not jitter at 9 to 10. */
.stp-capture__row-layer {
	flex: 0 0 auto;
	min-width: 2.5ch;
	color: var( --stp-ink-muted );
	font-size: 0.75em;
	font-variant-numeric: tabular-nums;
	text-align: right;
}

.stp-capture__row-acts {
	display: flex;
	flex: 0 0 auto;
	gap: 4px;
}

/* -------------------------------------------------------------------------
 * Tablet and up
 * ---------------------------------------------------------------------- */

@media ( min-width: 720px ) {

	.stp-capture__overlay {
		align-items: center;
		padding: 24px;
	}

	.stp-capture__dialog {
		max-height: calc( 100vh - 48px );
		border-radius: var( --stp-radius );
	}

	.stp-capture__body {
		flex-direction: row;
		align-items: flex-start;
	}

	.stp-capture__stage {
		flex: 1 1 45%;
		position: sticky;
		top: 0;
		min-width: 0;
	}

	.stp-capture__panel {
		flex: 1 1 55%;
		min-width: 0;
	}

	.stp-capture__footer {
		display: grid;
		grid-template-columns: 2fr 1fr;
		align-items: end;
	}

	.stp-capture__field--note {
		grid-column: 1 / -1;
	}

	/*
	 * The price's scope line, beside the price it qualifies.
	 *
	 * This footer is a two-column grid and the note was left to auto-placement
	 * when 0.9.0 made it a footer child, so it landed wherever the flow had a
	 * gap — the exact lottery the studio row map exists to prevent (9th review
	 * round, MINOR 7). Pinned, so it does not depend on which of the optional
	 * children happen to be present. Studio outranks this with its own row map
	 * (0-3-0 against 0-1-0), which is where the note belongs on that surface.
	 */
	.stp-capture__footer > .stp-capture__price-note {
		grid-column: 2;
		grid-row: 1;
	}

	.stp-capture__submit {
		width: auto;
		grid-column: 2;
		justify-self: end;
	}

	/* Opposite corner from "Add to cart", which is the whole point of it. */
	.stp-capture__startover {
		grid-column: 1;
		grid-row: 3;
		justify-self: start;
	}

	/*
	 * THE FACTS REGION, FULL WIDTH AND LAST — the same placement the studio map
	 * gives it, for the same reason, but here it is a row that is genuinely
	 * used: quick capture at desktop width opens the disclosure inline.
	 *
	 * Last rather than under the price, because this footer places most of its
	 * children by auto-placement and only pins three. A row numbered into the
	 * middle would either collide with whatever the cursor had already put
	 * there or, when the region is closed and contributes no row of its own,
	 * leave the gap behind — and a row after the last pinned one costs nothing
	 * while `display: none` holds. Focus moves into the region when it opens
	 * (toggleFacts), so it is found whatever it is beside.
	 */
	.stp-capture__footer > .stp-capture__facts-region {
		grid-column: 1 / -1;
		grid-row: 4;
	}

	/*
	 * THE WARNING STRIP AT DESKTOP WIDTH — the two-state map again, and for
	 * the same reason: pinned rows here would create a permanently empty row 1
	 * on every design that has nothing to check.
	 *
	 * An explicitly-placed grid item is placed before any auto-placed one
	 * whatever its `order`, so the strip's pin is what keeps it from colliding
	 * with the price note, which owns row 1 column 2 in the resting layout.
	 * The price is pinned alongside it rather than left to auto-placement:
	 * with row 1 taken by the strip the cursor would otherwise hand the first
	 * free cell to whichever optional child came next.
	 */
	.stp-capture__footer.has-warn > .stp-capture__warnstrip {
		grid-column: 1 / -1;
		grid-row: 1;
	}

	.stp-capture__footer.has-warn > .stp-capture__price {
		grid-column: 1;
		grid-row: 2;
	}

	.stp-capture__footer.has-warn > .stp-capture__price-note {
		grid-row: 2;
	}

	.stp-capture__footer.has-warn > .stp-capture__startover {
		grid-row: 4;
	}

	.stp-capture__footer.has-warn > .stp-capture__facts-region {
		grid-row: 5;
	}

	/* ---------------------------------------------------------------------
	 * Studio at desktop width: the rail stands up on the left and the panel
	 * becomes a drawer beside the canvas instead of a sheet under it.
	 * ------------------------------------------------------------------ */

	.stp-capture.is-studio .stp-capture__body {
		grid-template-columns: auto 1fr;
		grid-template-rows: 1fr;
		grid-template-areas: "rail stage";
	}

	/*
	 * The tool bar stands up into a left column and keeps the dark skin — the
	 * top bar still spans the card above it, so the two read as one frame around
	 * the garment rather than as a bar and a sidebar.
	 */
	.stp-capture.is-studio .stp-capture__rail {
		flex-direction: column;
		align-items: stretch;
		justify-content: flex-start;
		gap: 4px;
		/* Wider than it was: 26px icons over a full label. */
		width: 94px;
		padding: var( --stp-gap-sm );
		overflow-x: hidden;
		overflow-y: auto;
		border-top: 0;
		border-right: 1px solid var( --stp-light-12 );
	}

	/*
	 * The horizontal scroll affordance goes with the horizontal scroll. The rail
	 * is a column here and overflows downwards, where the fade would be a 28px
	 * band across the middle of the last tool — an artefact, not an affordance.
	 */
	.stp-capture.is-studio .stp-capture__rail::after {
		display: none;
	}

	.stp-capture.is-studio .stp-capture__rail-button {
		flex: 0 0 auto;
		min-width: 0;
		min-height: 66px;
		font-size: 11px;
	}

	/*
	 * STILL AN OVERLAY. "Beside the canvas" describes where it reads from, not
	 * a column the canvas gives up: the drawer shares the stage's grid area and
	 * slides in over it, so the canvas box is the same size open or closed. A
	 * drawer that took a column would resize the canvas, and every fitted text
	 * size on the stage is measured against that box.
	 */
	.stp-capture.is-studio .stp-capture__sheet {
		align-self: stretch;
		justify-self: start;
		width: var( --stp-drawer-w, 320px );
		max-width: 100%;
		height: auto;
		max-height: none;
		border-right: 1px solid var( --stp-border );
		border-radius: 0;
		box-shadow: 6px 0 24px var( --stp-shade-14 );
		transform: translateX( -100% );
	}

	.stp-capture.is-studio .stp-capture__sheet.is-open {
		transform: translateX( 0 );
	}

	/* A drawer does not come up from anywhere, so it has nothing to grip. */
	.stp-capture.is-studio .stp-capture__sheet-grip {
		display: none;
	}

	/*
	 * ...and the grip was carrying the panel's top spacing. On a phone it
	 * contributes 14px (8px margin, a 4px bar, 2px more) above a head whose own
	 * top padding is 4px. Hiding it here left the heading 13px from the drawer's
	 * top edge — and that edge sits flush against the mode-pill row, both
	 * surfaces white, the drawer's shadow horizontal only, so "Which area?" read
	 * as tucked up under the pills rather than as the title of the panel below
	 * them. Measured, not guessed: the gap between the two boxes is 0.
	 *
	 * The head takes the spacing back, at the same 16px the drawer already uses
	 * horizontally, so the heading sits inside a consistent inset instead of
	 * against the join.
	 */
	.stp-capture.is-studio .stp-capture__sheet-head {
		padding-top: var( --stp-gap );
	}

	.stp-capture.is-studio .stp-capture__stage {
		padding: var( --stp-gap );
	}
}

/* -------------------------------------------------------------------------
 * Motion + contrast preferences
 * ---------------------------------------------------------------------- */

@media ( prefers-reduced-motion: reduce ) {

	.stp-capture *,
	.stp-capture *::before,
	.stp-capture *::after {
		animation-duration: 0.001ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.001ms !important;
		scroll-behavior: auto !important;
	}

	/*
	 * The panel does not slide. With the duration flattened above, a transform
	 * that still ran would be a jump from off-screen to in-place — the worst of
	 * both. Dropping the transform entirely leaves the visibility flip, which is
	 * the panel simply being there: no travel, no motion to sit through.
	 */
	.stp-capture__sheet {
		transform: none !important;
	}

	/*
	 * The zone pulse does not run at all. capture.js already declines to add the
	 * class under this preference — a 1.2s introduction flattened to a
	 * one-frame flash is worse for the people the setting is for than no
	 * introduction — and this is the belt to that braces: a class arriving from
	 * anywhere else still animates nothing.
	 *
	 * The zones themselves are unaffected. They are still shown on hover, on
	 * focus, while a gesture runs and while the Areas panel is open; only the
	 * unprompted movement is gone.
	 */
	.stp-capture.is-visual.is-zones-pulse .stp-capture__zone::after {
		display: none;
		animation: none !important;
	}
}

@media ( forced-colors: active ) {

	.stp-capture__zone,
	.stp-capture__swatch,
	.stp-capture__pill,
	.stp-capture__tool,
	.stp-capture__rail-button,
	.stp-capture__sheet,
	.stp-capture__row,
	.stp-capture__row-pick--zone,
	.stp-capture__chrome-button,
	.stp-capture__pager-button {
		border: 1px solid CanvasText;
	}

	/*
	 * Forced colors drop --stp-chrome-bg and --stp-chrome-ink both: the bars
	 * become Canvas with CanvasText on them, which is right — the dark chrome
	 * was contrast, and this mode supplies its own. What they lose with it is the
	 * edge that separated them from the canvas, so it is drawn explicitly.
	 */
	.stp-capture__topbar,
	.stp-capture__rail,
	.stp-capture__pager,
	.stp-capture__dialog.is-inline {
		border: 1px solid CanvasText;
	}

	.stp-capture__chrome-button.is-on {
		outline: 2px solid Highlight;
		outline-offset: -1px;
	}

	/*
	 * ZONES ARE VISIBLE HERE, and deliberately so. Hiding them at rest is a
	 * decorative choice made against a photograph; forced colors is a
	 * declaration that decoration is not what this user is here for, so every
	 * outline and every label comes back. Nothing about where they are or what
	 * they do changes.
	 *
	 * THE SELECTOR HAS TO MATCH THE HIDING RULE'S, EXACTLY. The bare
	 * `.stp-capture__zone` in the border block above is one class; the rule that
	 * paints the outlines out at rest is a PAIR of selectors —
	 * `.stp-capture.is-visual .stp-capture__zone` (two classes) and
	 * `.stp-capture.is-visual .stp-capture__zone.is-filled` (three) — and
	 * specificity is not something a media query changes. So the restore silently
	 * lost, in the one mode where it mattered, and a forced-colors user in visual
	 * capture was looking at a garment with no visible targets on it at all:
	 * `border-color: transparent` is one of the few colors forced colors leaves
	 * alone.
	 *
	 * Both selectors are repeated here rather than just the shorter one, because
	 * dropping the `.is-filled` half would have left exactly the zones that
	 * already HAVE a decoration in them invisible — the worst half to lose.
	 * Matched at the same specificity and placed later in the file, this wins on
	 * source order, which is where a deliberate override belongs.
	 *
	 * Preview is still excepted — `.is-visual.is-preview` is three classes on the
	 * ancestor and still outranks both of these — because a proof with the
	 * apparatus drawn back over it is not a proof.
	 */
	.stp-capture.is-visual .stp-capture__zone,
	.stp-capture.is-visual .stp-capture__zone.is-filled {
		border: 1px solid CanvasText;
	}

	.stp-capture.is-visual .stp-capture__zone-label,
	.stp-capture.is-visual .stp-capture__zone-count {
		opacity: 1;
	}

	/*
	 * The rail's scroll fade is a gradient, and a gradient in forced colors is
	 * either ignored or painted as a flat system color over the tools it was
	 * meant to hint at. The overflow is still there and still scrollable; this
	 * mode has its own scrollbars to say so.
	 */
	.stp-capture__rail::after {
		display: none;
	}

	/*
	 * Forced colors drop the accent, so the open tool and the selected row are
	 * told apart by the system highlight — the same substitution the pressed
	 * parity tool makes below.
	 */
	.stp-capture__rail-button.is-on,
	.stp-capture__row.is-on {
		outline: 2px solid Highlight;
		outline-offset: -1px;
	}

	/*
	 * THE BAR IS THE DECAL; THE GRIP IS THE TARGET.
	 *
	 * This rule was written when the grip WAS the 40x4 bar. It is now a
	 * full-width, 28px-tall hit target with the bar as its ::before, so painting
	 * the container CanvasText drew a solid slab across the whole top of the
	 * sheet with the bar invisible inside it (9th review round, MINOR 11). The
	 * fill belongs on the pseudo-element that is meant to be seen; the target
	 * stays transparent, exactly as it is outside forced colors.
	 */
	.stp-capture__sheet-grip {
		background: transparent;
	}

	.stp-capture__sheet-grip::before {
		background: CanvasText;
		forced-color-adjust: none;
	}

	/*
	 * Forced colors drop the accent, so a pressed tool is told apart by its
	 * border and its system highlight rather than by a fill nobody will see.
	 */
	.stp-capture__tool[aria-pressed="true"] {
		outline: 2px solid Highlight;
		outline-offset: 1px;
	}

	.stp-capture__zone-count {
		color: CanvasText;
		background: Canvas;
		border-color: CanvasText;
		forced-color-adjust: none;
	}

	.stp-capture__swatch.is-on {
		outline: 3px solid Highlight;
		outline-offset: 1px;
	}

	/*
	 * Forced colors drop the accent and the halo both, so the stage
	 * affordances are redrawn in system colors: the frame in CanvasText, the
	 * handles filled with Highlight so the two things you can grab are the two
	 * things that stand out.
	 */
	.stp-capture__item::before {
		border-color: CanvasText;
		box-shadow: none;
	}

	.stp-capture__item:focus-visible::before,
	.stp-capture__item.is-selected::before,
	.stp-capture__item.is-active::before {
		border-color: Highlight;
		box-shadow: none;
	}

	.stp-capture__handle::after {
		background: Highlight;
		border-color: CanvasText;
		forced-color-adjust: none;
	}

	.stp-capture__handle--rotate::before {
		background: CanvasText;
		box-shadow: none;
		forced-color-adjust: none;
	}
}

/* Drawn size (wave 3): the read-only line that replaces the size menu when the
   drag decides the size. Same box as a field value so the panel does not jump. */
.stp-capture .stp-capture__size-drawn {
	display: block;
	padding: 0.5em 0;
	font-size: 16px;
	line-height: 1.4;
}

.stp-capture .stp-capture__size-drawn[hidden] {
	display: none;
}

/* ==========================================================================
 * Free placement (template setting `placement: free`, 1.0.0-rc4)
 *
 * No zone boxes at rest. One transparent button covers the garment; the
 * template's zones become soft areas that appear only while a decoration is
 * being dragged (.is-free-dragging), the nearest one lit; a vertical centre
 * guide shows while the drag is snapped to it; a quiet status line says when
 * the selected decoration is outside every usual area. Tokens only.
 * ======================================================================= */

.stp-capture__free {
	position: absolute;
	inset: 0;
	z-index: 1;
	width: 100%;
	height: 100%;
	min-height: 0;
	padding: 0;
	margin: 0;
	background: transparent;
	border: 0;
	border-radius: 0;
	cursor: crosshair;
	-webkit-tap-highlight-color: transparent;
}

.stp-capture__free:focus-visible {
	outline: 2px solid var( --stp-focus );
	outline-offset: -2px;
}

.stp-capture__area {
	position: absolute;
	z-index: 2;
	pointer-events: none;
	background: var( --stp-accent-tint-06 );
	border: 1px solid transparent;
	border-radius: var( --stp-radius-sm );
	opacity: 0;
	transition: opacity 0.15s ease;
}

.stp-capture.is-free-dragging .stp-capture__area {
	opacity: 1;
}

.stp-capture.is-free-dragging .stp-capture__area.is-near {
	background: var( --stp-accent-tint-14 );
	border-color: var( --stp-accent );
}

.stp-capture__guide {
	position: absolute;
	top: 0;
	bottom: 0;
	left: 50%;
	z-index: 3;
	width: 0;
	pointer-events: none;
	border-left: 1px solid var( --stp-accent );
	opacity: 0;
}

.stp-capture.is-free-snapped .stp-capture__guide {
	opacity: 0.9;
}

.stp-capture__free-hint {
	position: absolute;
	top: 8px;
	left: 50%;
	z-index: 4;
	max-width: calc( 100% - 24px );
	margin: 0;
	padding: 6px 10px;
	font-family: var( --stp-font-ui );
	font-size: 13px;
	line-height: 1.35;
	color: var( --stp-ink );
	text-align: center;
	pointer-events: none;
	background: var( --stp-light-95 );
	border: 1px solid var( --stp-notice-edge );
	border-radius: var( --stp-radius );
	box-shadow: var( --stp-shadow );
	transform: translateX( -50% );
}

.stp-capture__free-hint[hidden] {
	display: none;
}

.stp-capture.is-preview .stp-capture__free,
.stp-capture.is-preview .stp-capture__free-hint {
	display: none;
}

@media ( prefers-reduced-motion: reduce ) {
	.stp-capture__area {
		transition: none;
	}
}

/* ==========================================================================
 * Gallery presentation (1.0.0-rc4): the designer in the product gallery's slot.
 * The gallery element keeps its place in the layout; while it holds the
 * designer its own size limits are lifted so the card is never clipped.
 * ======================================================================= */

.stp-gallery-swapped {
	/* A sticky media column (common in product themes) taller than the window
	   would pin the designer's top out of reach; the swapped gallery scrolls
	   with the page instead. */
	position: relative !important;
	top: auto !important;
	height: auto !important;
	max-height: none !important;
	min-height: 0 !important;
	aspect-ratio: auto !important;
	overflow: visible !important;
	opacity: 1 !important;
}

.stp-gallery-swapped > :not( .stp-capture-gallery ) {
	display: none !important;
}

.stp-capture-gallery {
	position: relative;
	display: block;
	width: 100%;
	max-width: 100%;
	box-sizing: border-box;
}

.stp-capture-gallery[hidden] {
	display: none;
}

.stp-capture__gallery-back {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	min-height: 44px;
	margin: 0 0 8px;
	padding: 8px 14px;
	font-family: var( --stp-font-ui );
	font-size: 16px;
	line-height: 1.2;
	color: var( --stp-ink );
	cursor: pointer;
	background: var( --stp-surface );
	border: 1px solid var( --stp-border-strong );
	border-radius: var( --stp-radius );
}

.stp-capture__gallery-back::before {
	content: "\2039";
	font-size: 20px;
	line-height: 1;
}

.stp-capture__gallery-back:focus-visible {
	outline: 2px solid var( --stp-focus );
	outline-offset: 2px;
}

.stp-capture__thumbs {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
	margin-top: 8px;
}

.stp-capture__thumb {
	width: 64px;
	height: 64px;
	padding: 2px;
	cursor: pointer;
	background: var( --stp-surface );
	border: 2px solid var( --stp-border );
	border-radius: var( --stp-radius-sm );
}

.stp-capture__thumb[aria-pressed="true"] {
	border-color: var( --stp-accent );
}

.stp-capture__thumb:focus-visible {
	outline: 2px solid var( --stp-focus );
	outline-offset: 2px;
}

.stp-capture__thumb img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: contain;
}

.stp-capture-gallery.has-thumbs .stp-capture__pager {
	display: none;
}
