/* =========================================================================
   Upline Bones — the rules layer
   =========================================================================

   This stylesheet contains NOTHING about how a site looks. It contains only
   what every Upline-approved theme must do to work correctly with the Upline
   for WooCommerce plugin. A child theme supplies the design; this supplies the
   behaviour the design is not allowed to break.

   Everything here is either:
     - a hard rule from Build Spec §4 expressed in CSS, or
     - a motion or loading primitive that a child theme opts into by class, or
     - a structural fact about how WordPress assembles the document.

   Every colour in this file comes from a token. There is not one literal
   colour value below, on purpose: if a rule here needs a colour that no token
   provides, that is a missing token and it gets requested from the plugin
   (Build Spec §4.4) rather than invented here.

   WHAT IS NOT HERE ANY MORE — read this before adding a plugin selector
   ---------------------------------------------------------------------
   Versions up to v7 carried a large section headed "PLUGIN SURFACE HOOKS —
   VERIFIED": 49 rules, 14.8KB, styling .upline-menu, .upline-facet,
   .upline-mega, .upline-tile and their internal descendants by hand. It was
   63% of this file.

   It was correct when it was written, because the plugin then ignored theme
   tokens: the rail read none at all, and the token block was scoped to three
   classes, so a theme setting accent got it on whatever it thought to check
   and silently not on the rest. Hand-styling was the only route.

   upline-facets 1.13.0 fixed that. Every storefront surface is now inside the
   token scope, and a test in the plugin fails if its CSS and docs/theme-tokens.md
   stop agreeing in either direction. So that section became three problems at
   once: redundant, because tokens do the work; fragile, because half its
   selectors reached below the top-level classes the plugin guarantees, into
   __element and --modifier names it says will move; and actively harmful,
   because where a theme rule and a token both apply the theme wins, so a
   shop's own token setting does nothing — which is the exact failure 1.13.0
   was released to fix.

   It is deleted. Do not reinstate any of it. Per the plugin's own contract,
   needing a plugin selector is a bug report, not a theme technique: if a
   surface cannot be reached through the fifteen tokens, that is a missing
   token, and it gets raised against the plugin.

   Rules this file is held to:
     - No selectors into plugin markup at all.
     - No !important. Not one.
     - Never hide disabled filter options, never restyle the mobile toggle into
       a JS-dependent control, never change chip height.
     - Wide content scrolls in its own container, never the page.
   ========================================================================= */


/* -------------------------------------------------------------------------
   1. Foundations
   ------------------------------------------------------------------------- */

/* Hard rule §4.5, applied globally: the page never scrolls sideways. Anything
   that would force it scrolls inside itself instead. */
html,
body {
	overflow-x: clip;
}

/* The named escape hatch for wide content — spec tables, wide grids, anything
   the P2 two-column spec table brings. Applied as a block className. */
.ub-scroll-x {
	overflow-x: auto;
	overscroll-behavior-x: contain;
}

/* One focus treatment for every theme built on Bones, tokenised so a child
   changes it without writing CSS. Plugin surfaces inherit this too. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
	outline: var(--wp--custom--ub--focus-ring, 2px) solid var(--wp--custom--ub--focus-color, currentColor);
	outline-offset: var(--wp--custom--ub--focus-offset, 2px);
	border-radius: var(--wp--custom--upline--radius, 0px);
}

/* Minimum tap target, from the plugin's own `tap` token so a child and the
   plugin cannot disagree about it. Scoped with :where() so it contributes no
   specificity and a design can size a control up freely — it only ever raises
   a floor. Lowering `tap` is an accessibility regression; the plugin says so
   and this is where that statement lands in the theme. */
:where(.wp-block-button__link, .wp-element-button, button, [type="submit"], [type="button"]) {
	min-block-size: var(--wp--custom--upline--tap, 44px);
}


/* -------------------------------------------------------------------------
   2. Motion primitives
   -------------------------------------------------------------------------
   Bones owns the MECHANISM; a child theme owns the taste — durations and
   easings come from tokens, and a child opts a section in by adding a class.

   Two hard constraints shape all of it:
     - Build Spec §6.6 forbids layout shift on menu open, so nothing here
       animates a property that triggers layout. Opacity and transform only.
     - The theme ships no JavaScript, so nothing may depend on a script to
       become visible. The failure mode of an IntersectionObserver reveal
       (script never runs, content stays at opacity 0, page looks empty)
       cannot occur here: the only reveal rules live inside a support query
       whose animation END STATE is the normal rendered state.
   ------------------------------------------------------------------------- */

@media (prefers-reduced-motion: no-preference) {

	/* Scroll-driven reveal. Where animation-timeline is unsupported nothing is
	   applied at all and content is simply visible. */
	@supports (animation-timeline: view()) {
		.ub-reveal {
			animation: ub-rise linear both;
			animation-timeline: view();
			animation-range: entry 0% cover 24%;
		}

		/* Staggered children — cards, tiles, columns. Each child runs its own
		   view() timeline, so the stagger comes from their real positions
		   rather than hardcoded delays that go wrong at a different viewport
		   height. */
		.ub-reveal-items > * {
			animation: ub-rise linear both;
			animation-timeline: view();
			animation-range: entry 0% cover 20%;
		}
	}

	@keyframes ub-rise {
		from {
			opacity: 0;
			transform: translate3d(0, 14px, 0);
		}
		to {
			opacity: 1;
			transform: none;
		}
	}

	/* Panels that appear from display:none — mega menu, search dropdown. A
	   child opts in; the duration and easing come from tokens. */
	@keyframes ub-panel-in {
		from {
			opacity: 0;
			transform: translate3d(0, -6px, 0);
		}
		to {
			opacity: 1;
			transform: none;
		}
	}

	.ub-panel-in {
		animation: ub-panel-in var(--wp--custom--ub--motion-panel, 380ms) both;
	}
}

/* Reduced motion: neutralise every kind. The scroll-driven ones need their
   TIMELINE removed, not just a short duration, or they keep binding scroll
   position to opacity — which is why this is more than the usual blanket
   rule. */
@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation-duration: 0.001ms;
		animation-iteration-count: 1;
		transition-duration: 0.001ms;
		scroll-behavior: auto;
	}

	.ub-reveal,
	.ub-reveal-items > *,
	.ub-panel-in {
		animation: none;
		animation-timeline: auto;
		opacity: 1;
		transform: none;
	}
}


/* -------------------------------------------------------------------------
   3. Deferred rendering
   -------------------------------------------------------------------------
   WordPress core already handles image loading attributes: it marks the first
   in-viewport image fetchpriority="high", lazy-loads the rest, and adds
   decoding="async". Re-implementing that would fight core and would put logic
   in a theme that is meant to be dumb.

   What a theme owns is deferring the RENDER of off-screen sections, not just
   their images. content-visibility skips layout and paint until the user
   reaches the section; contain-intrinsic-size supplies a placeholder height so
   the scrollbar stays honest and in-page anchors still land.

   The other half — reserving space so a lazily-loaded image never shifts the
   page — is the child theme's job, because ratios are a design decision.
   ------------------------------------------------------------------------- */

.ub-defer {
	content-visibility: auto;
	contain-intrinsic-size: auto 640px;
}


/* -------------------------------------------------------------------------
   4. Page structure
   -------------------------------------------------------------------------
   Rules about how WordPress assembles the document, not about how any one
   theme looks. A child theme should never have to restate these.
   ------------------------------------------------------------------------- */

/* WordPress separates every top-level block with the root blockGap, and that
   includes the seam between the header template part and <main>. On a theme
   whose header is a solid band and whose first section is a full-bleed hero,
   the gap renders as a strip of page background wedged between them — a white
   bar under a black header, which reads as a bug rather than as spacing.

   Specificity note: WordPress emits its rule as
   `:where(.wp-site-blocks) > * + *`, which contributes NOTHING to specificity,
   so a single class here wins regardless of stylesheet order. */
.wp-site-blocks > main {
	margin-block-start: 0;
}

/* Sticky offset. Logged-in WordPress renders a fixed admin bar over the top of
   the viewport, so anything stuck to `top: 0` slides underneath it. Shops see
   0; we see 32 (or 46 on the narrow breakpoint, where the admin bar is taller
   and stops being fixed at all). Children position against this token instead
   of guessing. */
:root {
	--ub-sticky-top: 0px;
}

body.admin-bar {
	--ub-sticky-top: 32px;
}

@media screen and (max-width: 782px) {
	body.admin-bar {
		/* Below 782px the admin bar is position:absolute, not fixed — it
		   scrolls away with the page, so nothing needs to clear it. */
		--ub-sticky-top: 0px;
	}
}

/* The sticky header, and why it has to live HERE rather than in the skin.
   ------------------------------------------------------------------------
   A `wp:template-part` with `tagName:"header"` renders its own wrapper —
   `<header class="wp-block-template-part">` — around whatever the part file
   contains. A skin that also opens a group with `tagName:"header"` therefore
   produces two nested headers, and `position: sticky` on the INNER one does
   nothing at all: a sticky box can only travel inside its parent's padding
   box, and that parent is exactly as tall as it is. Zero travel. It scrolls
   away looking exactly like a sticky rule that was never written.

   Measured on bikeco before this rule existed: at scrollY 600 the header's
   top was -568px, i.e. gone. Wrapper height 181, inner height 181.

   So the sticky belongs on the wrapper, which only the parent can reach —
   a child theme cannot add a class to a template-part call inside a Bones
   template it inherits. `wp-block-template-part` is core-generated and
   core-published, the same class of dependency as Woo's `wc-block-*`, and
   unlike a plugin's `__element` names it does not move between releases.

   A design that does not want a sticky header sets `position: static` on
   this selector in its own stylesheet. Theme CSS is unlayered, so one plain
   declaration wins — no `!important`, no specificity games. */
.wp-site-blocks > header.wp-block-template-part {
	position: sticky;
	top: var(--ub-sticky-top, 0px);
	z-index: 40;
}


/* -------------------------------------------------------------------------
   5. Grid column ceiling
   -------------------------------------------------------------------------
   THE PROBLEM THIS EXISTS FOR
   ---------------------------
   A block grid set to "auto" columns emits
   `repeat(auto-fill, minmax(<min>, 1fr))`. That formula has no upper bound: it
   fits as many tracks as the container allows. At a 1480px wide measure a
   200px minimum yields SEVEN columns, so a four-item row fills the first four
   and leaves three empty tracks — which looks exactly like a section that has
   been left-aligned inside a container that is too wide. It is not an
   alignment bug and no amount of justify/margin fixes it; the row genuinely
   has seven cells.

   The fix is to raise the track FLOOR to one-Nth of the row. Once a track can
   never be narrower than a quarter of the available width, a fifth track can
   never fit, and `auto-fill` still steps down to 3, 2 and 1 as the container
   narrows. No media queries, no column counts to keep in sync.

   NOTE: for the plugin's OWN product grids this is not the right lever — the
   collection-grid column count is a plugin setting and should be set there.
   These classes are for grids the theme renders itself.

   Specificity note: WordPress's own rule is a single class
   (`.wp-container-core-group-is-layout-XXXX`) in an INLINE stylesheet that
   lands after the theme's linked file. A single class here would tie and lose.
   Every selector below is therefore at least two classes. This is written down
   so the next person does not rediscover it. */
/* TWO CORRECTIONS, Bones 2.3.8 — the ceiling was capping at N-1, everywhere.
   -------------------------------------------------------------------------
   1. THE GAP THE FORMULA SUBTRACTS MUST BE THE GAP THE GRID USES.

   `--ub-cap-gap` read `--wp--custom--upline--gap` — the theme's SPACING token
   — while the grid is laid out with `--wp--style--block-gap`, which is a
   different value. On bikeco: token **10px**, real gap **20px**. The floor is
   therefore computed one gap-size too generous, and a floor that is too large
   is a column that cannot fit:

       floor    (1480 - 3 x 10) / 4 = 362.5px
       available(1480 - 3 x 20) / 4 = 355px
       four tracks cannot fit, by 7.5px — `auto-fill` steps down to three

   Reported as "the confidence bar renders three columns with two entries
   stacked in the first", and that is exactly what a four-item row does in a
   three-track grid. But the defect is not that bar: **every capped grid on
   every site is one column short whenever the block gap exceeds the spacing
   token**, silently, because a grid that quietly caps at 3 looks like a
   deliberate 3-column grid. The formula subtracted a number it did not own.

   2. `auto-fill` -> `auto-fit`, because correcting (1) exposed what (1) had
   been accidentally hiding.

   The over-large floor was ALSO suppressing empty tracks: a three-item row in
   a cap-4 grid rendered three tracks, which looks right. Fix the gap alone and
   that row gains a fourth, empty track — the "section left-aligned inside a
   container that is too wide" look this section's own problem statement exists
   to kill. `auto-fit` collapses tracks with nothing in them, so a row shorter
   than its cap fills the width instead of trailing a dead column.

   Measured on the front page, both grids, before and after: the three-item row
   holds 480/480/480 with the fourth track collapsed to 0px and its right edge
   flush, and the four-item bar reads 355/355/355/355 on one row. Below roughly
   660px `--ub-cap-min` dominates both formulas identically, so the mobile
   stacking is arithmetically untouched — which is the constraint Ben set. */
.ub-cap-4.is-layout-grid,
.ub-cap-6.is-layout-grid {
	--ub-cap-gap: var(--wp--style--block-gap, var(--wp--custom--upline--gap, 16px));
}

.ub-cap-4.is-layout-grid {
	grid-template-columns: repeat(
		auto-fit,
		minmax(max(var(--ub-cap-min, 150px), calc((100% - 3 * var(--ub-cap-gap)) / 4)), 1fr)
	);
}

.ub-cap-6.is-layout-grid {
	grid-template-columns: repeat(
		auto-fit,
		minmax(max(var(--ub-cap-min, 120px), calc((100% - 5 * var(--ub-cap-gap)) / 6)), 1fr)
	);
}


/* -------------------------------------------------------------------------
   6. Archive shell
   -------------------------------------------------------------------------
   The two-column rail-and-grid arrangement that templates/archive-product.html
   uses. It is here rather than in a child because the DEFAULT archive lives
   here — but it is a default, not a decree.

   A child theme that wants a different shape (filters in a drawer only,
   filters across the top, no rail at all) replaces archive-product.html
   wholesale and never uses these classes. That is expected, not a workaround.

   The flex values are the "holy grail" arrangement: the rail holds its column
   width, the results take everything else, and the pair wraps to one column
   when there is no longer room for both — with no media query and no
   breakpoint to keep in sync with the design.
   ------------------------------------------------------------------------- */

/* The archive head sits at the wide measure but its CONTENT reads at the
   content measure, hard left. `layout: constrained` would do the capping and
   then centre the box in the wide container — which renders as a title
   floating in the middle of the page, not as a hero. `layout: default` plus a
   cap here keeps the left edge aligned with the rail below it. */
.ub-archive__head > * {
	max-inline-size: var(--wp--style--global--content-size, 720px);
	margin-inline: 0;
}

.ub-archive__body {
	display: flex;
	flex-wrap: wrap;
	gap: var(--wp--custom--upline--gap, 16px);
}

.ub-archive__rail {
	flex: 1 1 var(--wp--custom--upline--column, 16rem);
}

/* 28rem, not 20rem, and the reason is the stacking point rather than the
   width. These two flex bases decide when the pair wraps: it happens once
   rail-basis + results-basis + gap exceeds the container. At 20rem that lands
   near 590px, which leaves a window where the rail still holds its full column
   and the grid is squeezed to ~330px — one cramped product per row. At 28rem
   the pair stacks below roughly 720px instead, so the grid never renders
   narrower than about two columns before it goes full width. Measured, not
   guessed: side by side at 900 and 700, stacked at 390. */
.ub-archive__results {
	flex: 999 1 28rem;
	min-inline-size: 0;
}

/* Sticky rail on viewports tall and wide enough to benefit. Offset by the
   admin-bar token so it never slides under the toolbar. */
@media (min-width: 782px) {
	.ub-archive__rail > * {
		position: sticky;
		top: calc(var(--ub-sticky-top, 0px) + var(--wp--custom--upline--gap, 16px));
	}
}


/* -------------------------------------------------------------------------
   7. Form controls
   -------------------------------------------------------------------------
   Browsers ship a look for inputs, selects and buttons, and neither WordPress
   nor a block theme overrides it. So a bare <input> renders as OS chrome in
   the middle of a designed page — which is what the Upline search block and
   every Woo field currently do.

   This was briefly mistaken for a missing plugin token. It is not: no token
   would help, because the problem is that nothing in the stack normalises form
   elements at all. The fix belongs here, at element level, where it reaches
   every control on the site — the plugin's, Woo's and the theme's — without
   naming a single plugin class.

   Everything below inherits: font from the theme, colour from currentColor,
   border from the rule token, corners from the radius token. Bones states no
   opinion about how a field should look, only that it should look like the
   theme it is sitting in.
   ------------------------------------------------------------------------- */

:where(input, select, textarea) {
	font: inherit;
	color: inherit;
	background-color: transparent;
	border: 1px solid var(--wp--custom--upline--rule, currentColor);
	border-radius: var(--wp--custom--upline--radius, 0px);
	padding-block: var(--wp--custom--upline--gap-sm, 8px);
	padding-inline: var(--wp--custom--upline--gap, 16px);
	min-block-size: var(--wp--custom--upline--tap, 44px);
}

/* Checkboxes and radios are the exception: the rules above would blow them up
   to a 44px box. They keep their native rendering and only take the accent. */
:where(input[type="checkbox"], input[type="radio"]) {
	min-block-size: auto;
	padding: 0;
	border: 0;
	border-radius: 0;
	accent-color: var(--wp--custom--upline--accent, currentColor);
}

:where(input[type="search"]) {
	-webkit-appearance: none;
	appearance: none;
}

:where(select) {
	-webkit-appearance: none;
	appearance: none;
	/* Room for the caret drawn below, on whichever side the language runs. */
	padding-inline-end: calc(var(--wp--custom--upline--gap, 16px) * 2);
	background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
		linear-gradient(135deg, currentColor 50%, transparent 50%);
	background-position: right 1.05em top 55%, right 0.75em top 55%;
	background-size: 0.3em 0.3em, 0.3em 0.3em;
	background-repeat: no-repeat;
}

:where(input, select, textarea)::placeholder {
	color: var(--wp--custom--upline--muted, currentColor);
	opacity: 1;
}

/* A <button> that is not a block button — Woo's steppers, the plugin's search
   submit, anything a plugin renders raw. It inherits type and loses the OS
   chrome; a child theme decides what it looks like beyond that. */
:where(button):not(.wp-element-button):not(.wp-block-button__link) {
	font: inherit;
	color: inherit;
	background-color: transparent;
	border: 1px solid var(--wp--custom--upline--rule, currentColor);
	border-radius: var(--wp--custom--upline--radius, 0px);
	cursor: pointer;
}


/* -------------------------------------------------------------------------
   8. WooCommerce chrome
   -------------------------------------------------------------------------
   WHY THIS IS ALLOWED WHEN SECTION 4 WAS NOT

   Section 4 was deleted because it reached into upline-facets' internals, and
   that plugin's contract says everything below its top-level classes is
   private and will move. WooCommerce is the opposite case: `wc-block-*` is the
   surface Woo publishes for themes to style, it is stable across releases, and
   Woo's own block themes style it. Targeting it is the supported route, and
   there is no token alternative — Woo reads none of ours.

   The standard this exists to meet: Woo is the functional backend, not the
   front end. None of its default look may reach a customer. So this section
   REMOVES Woo's opinions and re-points them at theme tokens. It adds no design
   of its own — a child theme still decides how any of it looks.
   ------------------------------------------------------------------------- */

/* Woo hardcodes a blue focus ring and its own radius on form fields inside
   cart and checkout. Both are re-pointed at the theme.

   THE CLASS IS DOUBLED ON PURPOSE — 2.3.3.
   ------------------------------------------------------------------------
   Written singly, this rule lost. Measured on bikeco's live checkout: the
   email field computed `border-radius: 4px` with this stylesheet enabled AND
   with it disabled — identical — which is what a rule that never applies looks
   like. Woo ships a more specific declaration of its own.

   The token was NOT the problem, and that was worth proving rather than
   assuming, because "the var doesn't reach wc-block subtrees" would have been a
   contract finding against the plugin. It reaches: a probe element appended
   inside the field's own parent, with `border-radius: var(--wp--custom--upline--radius, 99px)`,
   computed **2px** — the sentinel never fired. Token fine, specificity losing.

   Repeating the class is specificity WITHOUT structural coupling. The three
   candidates measured — `.wp-block-woocommerce-checkout` ancestor,
   `.wc-block-checkout` ancestor, and this — all reached 2px. The ancestors were
   rejected for two reasons: each names markup above the field that Woo can
   restructure, which is the descendant-chain fragility this project gates
   against; and `.wc-block-checkout` is checkout-only, so cart's coupon field
   would have kept Woo's radius. Doubling the class Woo publishes couples to
   nothing but the name it already promises to keep. */
.wc-block-components-text-input.wc-block-components-text-input input[type="text"],
.wc-block-components-text-input.wc-block-components-text-input input[type="email"],
.wc-block-components-text-input.wc-block-components-text-input input[type="tel"],
.wc-block-components-text-input.wc-block-components-text-input input[type="number"],
.wc-block-components-select__select.wc-block-components-select__select,
.wc-block-components-textarea.wc-block-components-textarea {
	border-color: var(--wp--custom--upline--rule, currentColor);
	border-radius: var(--wp--custom--upline--radius, 0px);
	background-color: transparent;
	color: inherit;
}

/* Woo's checkout uses its OWN `checkout-header` template part, not the theme's
   — a deliberate distraction-free checkout, the same pattern Shopify uses. That
   is design, not omission, so it is STYLED here and never replaced: swapping the
   full site header back in would hand the shopper a nav bar at the exact moment
   they are deciding to pay.

   Structure only, from tokens. A child theme still decides how the wordmark
   itself looks; this only stops the part rendering as unstyled text on nothing.
   Scoped by Woo's published `woocommerce-checkout` body class so it cannot
   reach any other header.

   `position: static` un-sticks it, and that is a mechanic rather than a taste
   (Bones 2.3.5). Section 4 makes `.wp-site-blocks > header.wp-block-template-part`
   sticky, and that rule is written for the header the SKIN renders: on every
   other page the wrapper itself paints nothing and the skin's own header group
   inside it supplies the opaque surface. Checkout renders Woo's `checkout-header`
   part instead, so the skin's header group is not in the document at all — which
   leaves a sticky, transparent, z-40 box with nothing painting it. Body content
   scrolls straight through it. Reported from the live walk as "transparent, or
   renders below the body content"; one mechanism, both descriptions.

   The other candidate fix was to paint this wrapper opaque and keep it sticky.
   Rejected: it would hand distraction-free checkout a persistent bar nobody
   asked for, and it would mean Bones imposing sticky behaviour on a part it
   neither authors nor paints — the same class of error as walking into markup
   you do not own. A skin that wants a sticky checkout header declares
   `position: sticky` and its own background in its own unlayered CSS, the
   symmetric escape hatch to the one documented at the section 4 rule.

   `:not(.woocommerce-order-received)` is the scope correction of Bones 2.3.6,
   and it is the law above caught one register out. Woo puts
   `woocommerce-checkout` on the body for the WHOLE checkout FLOW — the form
   step AND the order-received endpoint — but only the form step renders the
   `checkout-header` part. MEASURED on the live confirmation page: it renders
   the full site header (`.rl-header` inside this wrapper) and the full site
   footer, so 2.3.3's padding and hairline had been landing on the SKIN's
   header there, and 2.3.5's `position: static` went on to un-stick it.

   A body class marks the FLOW; this rule needed the TEMPLATE. Both classes are
   Woo-published and stable, so the discriminator costs one token and couples
   to nothing structural. */
.woocommerce-checkout:not(.woocommerce-order-received) > .wp-site-blocks > header.wp-block-template-part,
.woocommerce-checkout:not(.woocommerce-order-received) header.wp-block-template-part {
	position: static;
	padding-block: var(--wp--preset--spacing--50, 1rem);
	padding-inline: var(--wp--preset--spacing--50, 1rem);
	border-block-end: 1px solid var(--wp--custom--upline--rule, currentColor);
	margin-block-end: var(--wp--preset--spacing--70, 2rem);
}

/* The checkout trust strip (templates/page-checkout.html).
   ------------------------------------------------------------------------
   Woo's own page-checkout ships header + main and NO footer, so a shopper on
   the page where they decide whether this shop is real has no return path, no
   policy, no way to ask a question. That is the one unambiguous gap on the
   page; the rest of Woo's minimal checkout is deliberate and is left alone.

   The three items are TEXT, not links, and that is not an oversight. A returns
   page and a contact page do not exist on every shop — on bikeco they do not
   exist yet — and a trust strip whose links 404 is the anti-trust strip. Build
   spec: never fake a function. Each item carries its own slot class so the day
   a shop has the page, the text becomes a link in a named container and nothing
   about the layout moves. */
.ub-checkout-trust {
	border-block-start: 1px solid var(--wp--custom--upline--rule, currentColor);
	color: var(--wp--custom--upline--muted, inherit);
	text-align: center;
}

/* Product tile image alignment — Bones 2.3.7.
   ------------------------------------------------------------------------
   The image box is a design ratio the skin sets; the image inside it is
   whatever ratio Woo's Product Image block emits — and it emits that ratio as
   an INLINE STYLE on the tag (`aspect-ratio:1/1`), which no stylesheet rule
   can reach without `!important`. So a skin cannot make the two agree by
   styling alone, and the mismatch has to be absorbed rather than argued with.

   Measured on bikeco before this rule: a 4:5 box at 373px holding a 1:1 image
   at 298px, the image flush to the TOP, with 75px of dead ground below every
   card in the grid. Centred it reads 37px above, 37px below.

   This is the chassis's business and not the skin's: the numbers come from the
   skin's ratio and the block's attribute, but "a shorter image sits in the
   middle of its box, not jammed against the top" holds for every ratio and
   every design that will ever inherit from here. Nothing about it is a look.

   The anchor is the flex item, not the image — Woo wraps the image in a link —
   so it takes the full inline size and the image centres inside it. */
.wc-block-components-product-image {
	display: flex;
	align-items: center;
	justify-content: center;
}

.wc-block-components-product-image > a {
	display: block;
	inline-size: 100%;
}

/* Woo's notices ship a green/blue/red palette and an icon column. The colour
   comes back to the theme; the structure is left alone because it carries
   meaning. */
.wc-block-components-notice-banner {
	border-radius: var(--wp--custom--upline--radius, 0px);
	border-color: var(--wp--custom--upline--rule, currentColor);
	color: inherit;
}

/* The quantity stepper and its buttons — OS chrome by default. */
.wc-block-components-quantity-selector {
	border-color: var(--wp--custom--upline--rule, currentColor);
	border-radius: var(--wp--custom--upline--radius, 0px);
}

.wc-block-components-quantity-selector__button {
	color: inherit;
	background: transparent;
	border: 0;
	min-inline-size: var(--wp--custom--upline--tap, 44px);
}

/* Woo's own button classes, so a cart or checkout CTA is the theme's button
   rather than Woo's. Colour is deliberately not set here — that is design, and
   it comes from the child through the button element styles. */
.wc-block-components-button:not(.is-link) {
	border-radius: var(--wp--custom--upline--radius, 0px);
	min-block-size: var(--wp--custom--upline--tap, 44px);
}

/* Woo draws its own hairlines in a hardcoded grey. */
.wc-block-components-totals-wrapper,
.wc-block-cart-items,
.wc-block-components-order-summary,
.wc-block-components-shipping-rates-control {
	border-color: var(--wp--custom--upline--rule, currentColor);
}

/* Woo's price strikethrough and sale colours. */
.wc-block-components-product-price__regular,
.wc-block-components-product-metadata__description {
	color: var(--wp--custom--upline--muted, currentColor);
}


/* -------------------------------------------------------------------------
   9. Icon slots
   -------------------------------------------------------------------------
   The chassis does not own an icon LOOK — it owns the invariant that a theme
   has exactly one of them.

   The header's controls come from three different owners: the search toggle
   and the menu toggle are the plugin's, the account link and the mini-cart are
   WooCommerce's. Left alone, each ships its own glyph in its own style, which
   is how one header ends up with a thin-line magnifier, a filled material
   person and a solid cart — measured on bikeco, three families in a row of
   three. No skin can fix that by choosing a nice icon, because the glyphs
   arrive from vendors the skin does not control.

   So Bones publishes SLOTS, not a pack. Each slot is a token carrying a glyph;
   this file masks the vendor's own icon out and paints the slot in its place,
   in `currentColor`, at one size. A skin swaps the whole family by declaring
   the slots in its `theme.json` — one file, one place, all of them together.

   The identity stays the skin's: every theme picks a family that matches its
   design, and two themes are not expected to agree. What is invariant is
   WITHIN a theme — one family, one optical weight, one baseline. The default
   family below exists so an unskinned site is already consistent, not to be
   anybody's design.

   Flat keys, single-dash variables, for the reason the token contract gives:
   a nested `icon: { search }` would emit `--wp--custom--ub--icon--search`,
   which nothing reads, and the slot would silently fall back to the vendor
   glyph — the exact failure this section exists to prevent.

   Slots: search, account, cart, menu, close. Adding a surface means adding a
   slot here AND to the gate's list, so a half-added slot fails loudly instead
   of rendering one vendor icon among five theme ones.
   ------------------------------------------------------------------------- */

.ub-icon,
.upline-menu__toggle .upline-menu__toggle-icon,
.upline-search__toggle .upline-search__toggle-icon,
.wc-block-customer-account__link .wc-block-customer-account__visual,
.wc-block-mini-cart__button .wc-block-mini-cart__quantity-badge::before {
	display: block;
	inline-size: var(--wp--custom--ub--icon-size, 20px);
	block-size: var(--wp--custom--ub--icon-size, 20px);
	background-color: currentColor;
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-position: center;
	mask-position: center;
	-webkit-mask-size: contain;
	mask-size: contain;
}

/* Paint the slot on the vendor's OWN icon box, never on an extra pseudo-element
   beside it.
   ------------------------------------------------------------------------
   2.3.0 added a `::before` next to each vendor glyph and hid the glyph. Two
   things went wrong and both are visible in one screenshot: the vendor `<svg>`
   did not hide, so every control rendered two icons; and even if it had, the
   emptied vendor box would still have occupied its space beside the new one.

   The hiding failed because `display` was being argued at equal specificity.
   `.wc-block-customer-account__link svg` is (0,1,1) and Woo's own rule is too,
   so the later sheet won — and which sheet is later is not something a theme
   should be betting on. Naming the vendor's icon container as well takes it to
   (0,2,1) and settles it by specificity instead of by load order. The same
   correction applies to every selector in this section, which is why they are
   all two classes deep now.

   That is a doctrine point, not a preference. A tie broken by stylesheet order
   is meaning carried by TIMING: Woo can reorder its enqueues in any release
   and the hide flips with no diff on our side and nothing to review. Two
   classes deep puts the outcome in an explicit field of the selector, where it
   can be read and argued with. Prefer the field over the ordering wherever a
   tie is possible.

   The plugin's toggle glyphs are DRAWN, not shipped as files: the menu icon is
   three background bars, and the magnifier is a `1.1em` circle made of
   `border: 2px solid currentColor` with a rotated `::after` for the handle.
   Killing the pseudo-elements alone left that border ring behind — which reads
   as a thin outline box around the search control — and left the span at
   1.1em rather than the family's size. So the border and the radius are
   cleared explicitly, and the size is restated here rather than inherited.

   The general shape, because it will recur on the next drawn glyph: **a glyph
   built from several primitives has to be suppressed as an ANATOMY, not as a
   node.** Enumerate what draws it — background, border, radius, ::before,
   ::after — and clear all of them. Clearing some leaves the remainder reading
   as something it never was: here a lens circle became an apparent button
   outline, and the person judging the header saw a bigger box with a smaller
   glyph in it and had no way to know those were one element.

   That is gate 16's failure one level down. Partial override across slots is
   invisible per element and shows only as mixed families; partial suppression
   within a glyph is invisible as a defect and shows only as a wrong
   impression. Neither errors. Both are found by a human squinting, which is
   the thing the gates exist to stop being the detector. */
.upline-menu__toggle .upline-menu__toggle-icon,
.upline-search__toggle .upline-search__toggle-icon {
	border: 0;
	border-radius: 0;
}

.upline-menu__toggle .upline-menu__toggle-icon::before,
.upline-menu__toggle .upline-menu__toggle-icon::after,
.upline-search__toggle .upline-search__toggle-icon::before,
.upline-search__toggle .upline-search__toggle-icon::after {
	display: none;
}

.upline-menu__toggle .upline-menu__toggle-icon {
	-webkit-mask-image: var(--wp--custom--ub--icon-menu);
	mask-image: var(--wp--custom--ub--icon-menu);
}

/* Open state swaps the same slot for the close glyph, so the control reads as
   one thing that changes rather than two controls in a row. */
.upline-menu__toggle[aria-expanded="true"] .upline-menu__toggle-icon {
	-webkit-mask-image: var(--wp--custom--ub--icon-close);
	mask-image: var(--wp--custom--ub--icon-close);
}

.upline-search__toggle .upline-search__toggle-icon {
	-webkit-mask-image: var(--wp--custom--ub--icon-search);
	mask-image: var(--wp--custom--ub--icon-search);
}

/* The drawer's close glyph — the slot system finally reaching the control a
   phone shopper actually touches, Bones 2.3.9.
   ------------------------------------------------------------------------
   Until plugin 1.26.3 this could not be done as a slot. The glyph was a
   16.5 x 2px painted bar, rotated 45 degrees, with a second arm on a
   pseudo-element — so one theme shipped TWO close glyphs, the header toggle
   wearing `iconClose` and the drawer wearing the plugin's X. Gate 16 could
   not see it: it checks that slots are DECLARED, not that the plugin USES
   them.

   1.26.3 rebuilt it as a born-box, and the anatomy was re-enumerated on the
   served bytes rather than taken from the changelog — the rule this section
   already states, and a habit the previous close-icon reading earned the hard
   way. Measured in the open drawer at 390: **square 16.5 x 16.5, no paint, no
   rotation on the span, both arms on `::before` and `::after`, no border, no
   radius, no shadow.** All four claims held.

   CORRECTED 2026-09-13, and the correction is the more useful half.

   This block originally claimed `transform` was load-bearing: that the plugin
   centred the span with `translate(-8.25px, -8.25px)`, half of 16.5, so
   restating the box at 20px would leave it 1.75px short on each axis. **That
   was wrong, and it was wrong by reading the interface instead of the
   artifact.** The plugin's own declaration is:

       .upline-menu__close-icon { position: absolute; top: 50%; left: 50%;
         width: 1.1em; height: 1.1em; transform: translate(-50%, -50%); }

   `getComputedStyle` resolves a percentage translate against the box, so
   `translate(-50%, -50%)` on a 16.5px span computes to
   `matrix(1, 0, 0, 1, -8.25, -8.25)` — indistinguishable from a hard-coded
   pixel value by reading the computed output. The number was right; the
   AUTHORSHIP was invented. Doctrine rule 4, on this file's own author.

   So the `transform` line below restates the plugin's value verbatim and
   absorbs nothing, and percentage centring re-centres itself when the box
   changes — which is the whole virtue of it. Same for `border` and
   `border-radius`: the plugin declares neither on this span (the magnifier is
   the one with a ring). Three declarations of furniture, kept only until the
   next icon-touching release deletes them, because a restated value becomes a
   conflict the day the other side changes its approach.

   WHAT THIS BLOCK ACTUALLY ABSORBS, checked against the plugin's source
   declarations rather than against computed values:
     - `inline-size`/`block-size`  the plugin's span is `1.1em`; this
                                   normalises it to the icon family's box
     - `background-color`          the span itself is UNPAINTED — only the two
                                   pseudo arms carry `currentColor` — so this
                                   supplies the paint the mask needs
     - the mask slot               the point of the exercise
     - `::before`/`::after: none`  two 2px arms rotated +/-45deg that would
                                   otherwise cross the masked glyph

   The borrowed number everyone went looking for is not here at all. It is on
   the TOGGLE: `.upline-menu__toggle-icon::before { top: -6px }` and
   `::after { top: 6px }`, on a span that is itself `1.1em x 2px` — a bar.
   Section 9's pseudo-suppression plus the box restatement is what absorbs
   that, and no offset arithmetic of ours is involved.

   Verified with the shipped rule text injected into the live drawer: box
   20 x 20 matching the magnifier, 12px clear on all four sides of the 44px
   control, mask applied, both arms suppressed, paint arriving as
   `currentColor` — white, inherited from the drawer.

   THE MEDIA QUERY, AND WHY IT IS 760 AND NOT 782 — Bones 2.3.10.

   2.3.9 shipped these rules unscoped, and that was a defect on the live site:
   the close control EXISTS IN THE DOM AT EVERY WIDTH and only means something
   while the drawer does. Above the drawer's breakpoint the plugin collapses it
   — measured before 2.3.9 as `display: inline`, 0 x 0, no mask — and the slot
   rules handed that collapsed control a 20px box, `currentColor` paint and a
   glyph. Reported from Ben's desktop: the drawer's X floating above the left
   end of the nav, menu closed. A rule about the drawer needs the drawer's
   context.

   The boundary is the PLUGIN'S. CITED, as of the re-stamp, against the
   plugin's published boundary table — "The layout boundaries", tokens doc
   stamped 1.27.1 — which names `max-width: 760px` / `min-width: 761px` for the
   menu surfaces and calls these numbers API. The table's own words: using the
   rail's boundary for the menu's "is wrong by 22px".

   It was originally EXCAVATED here by reading `components.css`, which carried
   exactly two width queries with all eight `__close` rules inside the first.
   The excavation was right and the numbers never moved — but the table's
   phrasing is the point: "excavating a number is not the same as being
   promised one". The promise now carries a bidirectional suite assertion and a
   release marker, so this citation stops aging on releases that do not touch
   boundaries, and the bench that can change the fact owns keeping it true. **Mirroring this file's own 782px would have been
   wrong by 21 pixels:** measured live in a 770px frame, the plugin is in
   desktop layout (the label is an inline 130 x 20 box, not the 44px control)
   while a 782-scoped rule would still have applied — the same floating glyph,
   in a narrower band, where nobody would have looked for it.

   782 is WordPress's breakpoint. 760 is the plugin's. The rule belongs to the
   plugin's drawer, so it takes the plugin's number. */
@media (max-width: 760px) {
	.upline-menu__close .upline-menu__close-icon {
		display: block;
		inline-size: var(--wp--custom--ub--icon-size, 20px);
		block-size: var(--wp--custom--ub--icon-size, 20px);
		background-color: currentColor;
		border: 0;
		border-radius: 0;
		transform: translate(-50%, -50%);
		-webkit-mask-repeat: no-repeat;
		mask-repeat: no-repeat;
		-webkit-mask-position: center;
		mask-position: center;
		-webkit-mask-size: contain;
		mask-size: contain;
		-webkit-mask-image: var(--wp--custom--ub--icon-close);
		mask-image: var(--wp--custom--ub--icon-close);
	}

	.upline-menu__close .upline-menu__close-icon::before,
	.upline-menu__close .upline-menu__close-icon::after {
		display: none;
	}
}

/* Woo ships real `<svg>` children inside a wrapper. The wrapper becomes the
   slot's canvas and the glyph inside it is hidden, so no box is added and
   nothing shifts. Accessible names live in text, not in these glyphs. */
.wc-block-customer-account__link .wc-block-customer-account__visual svg,
.wc-block-mini-cart__button .wc-block-mini-cart__quantity-badge svg {
	display: none;
}

.wc-block-customer-account__link .wc-block-customer-account__visual {
	-webkit-mask-image: var(--wp--custom--ub--icon-account);
	mask-image: var(--wp--custom--ub--icon-account);
}

/* The cart is the one that cannot take the mask directly: its badge carries the
   item-count bubble as a child, and masking the badge would mask the count with
   it. The glyph goes on a pseudo-element filling the badge instead, so the
   count stays anchored to the icon exactly where Woo puts it. */
.wc-block-mini-cart__button .wc-block-mini-cart__quantity-badge {
	position: relative;
	inline-size: var(--wp--custom--ub--icon-size, 20px);
	block-size: var(--wp--custom--ub--icon-size, 20px);
}

.wc-block-mini-cart__button .wc-block-mini-cart__quantity-badge::before {
	content: "";
	position: absolute;
	inset: 0;
	-webkit-mask-image: var(--wp--custom--ub--icon-cart);
	mask-image: var(--wp--custom--ub--icon-cart);
}


/* -------------------------------------------------------------------------
   10. Inline-menu structural compensation
   -------------------------------------------------------------------------
   Moved up from Ridgeline, where it had no business being: this is structure,
   not look, and every Upline theme that puts the menu inline in the bar needs
   it identically.

   The mega panel opens at a fixed offset from its containing block, so it
   begins level with the bottom of the bar row. The menu's own list is shorter
   than the row and gets centred in it, which leaves a horizontal band of bar
   belonging to no list item, sitting directly between the link and the panel
   it opens. A pointer crossing that band loses `li:hover` and the panel shuts
   before it is reached. Measured on bikeco before the fix: a 10px dead zone;
   after, 1px, with the link box growing 44px -> 62px, still above the tap
   floor.

   Desktop only. A drawer toggle is not a hover target, and stretching a nav
   around one at phone widths is how the toggle ends up 661px wide.

   KNOWN LIMIT, and it is the plugin's to close: this only bridges the hovered
   item's own column. The panel spans the viewport while the item is ~81px
   wide, so every other column keeps a ~63px band belonging to neither box —
   reach diagonally and hover still goes false mid-traverse. The plugin's
   1.21.0 grace period is what actually makes that survivable; this rule closes
   the vertical gap, not the diagonal one. */
@media (min-width: 782px) {
	.ub-inline-menu,
	.ub-inline-menu nav,
	.ub-inline-menu nav > div {
		block-size: 100%;
	}

	.ub-inline-menu {
		align-self: stretch;
	}

	.ub-inline-menu nav > div > ul {
		block-size: 100%;
		align-items: stretch;
	}
}
