/* =========================================================================
   Ridgeline — the design layer
   =========================================================================

   Loads after upline-bones.css. Bones supplies the behaviour every Upline
   theme must have; this supplies how Ridgeline looks.

   The division, stated plainly because it is what keeps the system a system:
   if a rule would be true of EVERY Upline theme, it belongs in Bones. If it is
   true only of this design, it belongs here.

   WHAT THIS FILE MAY NOT DO
   -------------------------
   - No selectors into upline-facets markup (.upline-*). Not one. As of plugin
     1.15.0 the plugin's feel rules live in `@layer upline`, so an unlayered
     rule here would WIN with a single plain class and no !important. That is
     not permission. @layer removed the specificity trap and nothing else; the
     fragility ban stands, because __element and --modifier names still move
     between releases. Anything a token cannot reach is a token request.
   - No !important. Not one, anywhere, ever. There is no longer any case for it.
   - NEVER wrap this file in @layer. Unlayered theme declarations beating the
     plugin's layered ones is the mechanism the whole ownership line runs on —
     an unlayered stylesheet is load-bearing here, not untidy. (Build Spec
     hard rule 7.)
   - Never change chip height (36px), never hide disabled facet options, never
     make the mobile menu toggle depend on JavaScript.

   IF YOU EVER WRITE CHOREOGRAPHY, ANCHOR ON THE HOOKS
   ---------------------------------------------------
   The state hooks are the API; the elements currently carrying them are not.
   Anchor on `[data-upline-open]` as an attribute selector and on a bare
   `.is-open` with structural combinators — never on `.upline-mega` or
   `.upline-menu__drawer`, which are the carriers the docs happen to name today
   and may be renamed tomorrow. Hook-anchored choreography survives a rename;
   element-anchored choreography dies silently.

   MOBILE FIRST
   ------------
   Every base rule below is the 390px case. Every media query is min-width.
   Nothing here was designed wide and squeezed down.

   BROWSER FLOOR
   -------------
   Below Safari 15.4 / Chrome 99 / Firefox 97, @layer is unsupported and the
   plugin's layered styles drop entirely, rendering its surfaces unstyled. Check
   the browser version before investigating any "unstyled menu" report, or you
   will go hunting in CSS for a browser problem.

   OPEN TOKEN REQUESTS — do not work around these
   ----------------------------------------------
   Both are raised against the plugin and both would be one unlayered line to
   "fix" here. Fixing them here papers over the exact gap the request exists to
   close, and the request then quietly dies. Ship the surface imperfect.
     1. panelShadow — the mega panel's rgba(0,0,0,0.35) 0 14px 34px -18px is
        unreachable, and Ridgeline's rule is outlined-never-shadowed.
     2. Panel radius wiring — the theme sets 2px, the panel computes 0px and
        ignores an override. Reads as a one-surface wiring gap, not a missing
        token.

   Resolved since 1.14.0: menu motion. The plugin no longer toggles `display`;
   panels transition through grid-template-rows/opacity/visibility/transform,
   driven by motionDuration, motionEasing, menuHiddenTransform and
   drawerHiddenTransform. It enforces prefers-reduced-motion itself, at
   important priority — this file does not need to write it.
   ========================================================================= */


/* -------------------------------------------------------------------------
   1. The mono treatment — Ridgeline's signature
   -------------------------------------------------------------------------
   Uppercase, tight size, wide tracking. It appears on eyebrows, counts, chips,
   button labels and meta. Defined once as a class and inherited by the h6
   element style in theme.json, rather than repeated at every call site.
   ------------------------------------------------------------------------- */

.rl-mono,
.rl-eyebrow {
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--mono-sm);
	font-weight: 500;
	letter-spacing: var(--wp--custom--rl--mono-track, 0.14em);
	text-transform: uppercase;
	line-height: 1.3;
}

.rl-eyebrow {
	color: var(--wp--custom--rl--accent-ink);
}

/* Accent as INK wants the opposite lightness from accent as FILL, and once the
   fill end was pinned by white text (accent #d63700, 4.77:1) the ink end could
   no longer be the same value on dark grounds. Measured: #d63700 on the inverse
   ground drops to 4.13 stark / 3.77 warm — a regression the fill change caused.

   So ink is two derived values, neither shop-pickable, both in the private rl
   namespace because they are computed from the accent rather than chosen:
     accentInk        #c33200  light grounds  — 5.56 stark base / 4.57 warm surface
     accentOnInverse  #ff4d14  dark grounds   — 5.93 stark inv  / 5.41 warm inv
   The dark-ground value is Ben's original vivid orange, retained exactly where
   it still works. Nothing was lost by darkening the fill; the old value moved
   to the surface it was always right for.

   accentInk is solved against the DARKEST light ground, not the lightest: warm
   editorial's `surface` (#efe8de) binds at 4.57, and base (#faf7f2) would have
   licensed a value three steps lighter that failed inside every surface band.
   tools/contrast-audit.mjs reads both palettes out of the JSON and enumerates
   the ground as a dimension, so "it passes on white" can no longer stand in for
   "it passes".

   The eyebrow appears on both kinds of ground (services is light; hero and the
   newsletter band are inverse), so the light value is the default and the dark
   containers override it. Link hover is the same story: theme.json sets the
   light-ground ink globally on elements.link:hover, and the same container set
   flips it, because a global hover colour reaches links inside dark bands too.
   The footer was already carved out; the others were not, which is the defect
   the ground-aware audit surfaced. */
.rl-announce .rl-eyebrow,
.rl-footer .rl-eyebrow,
.rl-section--inverse .rl-eyebrow,
.rl-hero .rl-eyebrow,
.rl-panel--dark .rl-eyebrow,
.rl-announce a:hover,
.rl-section--inverse a:hover,
.rl-hero a:hover,
.rl-panel--dark a:hover {
	color: var(--wp--custom--rl--accent-on-inverse);
}


/* -------------------------------------------------------------------------
   2. Header
   -------------------------------------------------------------------------
   The bar is the theme's own markup, so its stickiness and its stuck-state
   transition are ours. The mega panel inside it is the plugin's and is
   untouched — see KNOWN PLUGIN GAPS 1.

   Sticky is CSS, so it works with JavaScript off. The compact stuck state is
   layered on top with a scroll-driven animation where the browser supports
   one, and simply does not happen where it does not. Nothing depends on a
   script, and nothing shifts layout: the bar's height is animated on a
   container that already reserves its own space.
   ------------------------------------------------------------------------- */

/* The sticky header: DELETED FROM THIS FILE at 2.11.2, expiry fired.
   ------------------------------------------------------------------------
   This file carried an identical copy of Bones' sticky rule from 2.2.1, as a
   bridge: the rule belongs in Bones — a child cannot add a class to a
   template-part call inside a template it inherits — but a parent-theme update
   is a separate install step, and bikeco sat on Bones 2.2.0 for the whole of
   that period, which would otherwise have left the site with the sticky
   removed from the skin and not yet added to the chassis.

   The bridge carried a stated expiry: remove the copy once the served parent
   is Bones >= 2.2.1, read from the artifact the site hands out rather than
   from the version that left this bench. That read ran 2026-09-12, same-origin
   against `/wp-content/themes/<slug>/style.css` with a per-request buster and
   `cache: no-store`: Bones 2.3.1, Ridgeline 2.11.1, both HTTP 200. Expiry
   satisfied, copy deleted, and the rule now lives in exactly one place —
   upline-bones.css section 4.

   This is the seam rule from the other direction: a rule that moved up and was
   left down became an opponent (the doubled icons, 2.10.0). This one was left
   down ON PURPOSE, with a written end date and a named condition, which is the
   whole difference between a bridge and a fork. The deletion is what makes the
   difference true rather than claimed. */

/* =========================================================================
   2.15.0 — THE HEADER'S MARKUP IS NO LONGER THIS THEME'S. READ THIS FIRST.
   =========================================================================

   `parts/header.html` used to hold Ridgeline's own bar. It now holds Ridgeline's
   own CHROME — the announce strip and the `rl-header` wrapper — around a single
   plugin block, `upline-facets/header-option`, which renders whichever of Bones'
   six header patterns the shop has chosen. So every slot in the bar is Bones'
   markup now, and this file styles the chassis's slot classes instead of its own.

   THE RENAME MAP, so the measurements below stay readable:

     .rl-bar          -> .ub-header                    the white band
     .rl-bar__row     -> .ub-header__brand-row > div   the wrapping row
                         .ub-header__strip > div       (and the category row)
     .rl-bar__row     -> .ub-collapse                  resting/stuck heights only
     .rl-brand        -> .ub-header__logo
     .rl-wordmark     -> .ub-header__logo              (img / a inside it)
     .rl-nav          -> .ub-inline-menu
     .rl-actions      -> .ub-header__actions
     .rl-search       -> .ub-header__search
     (new)               .ub-header__contact, .ub-header__secondary

   `.rl-header` and `.rl-announce` STAY, and `.rl-header` staying is deliberate
   rather than leftover: its `position: relative` is the mega panel's containing
   block, measured, and Bones sticks the OUTER template-part wrapper while leaving
   `.ub-header` unpositioned — so keeping this element keeps the panel resolving
   exactly where it resolves today. Dropping it would have handed the panel to a
   sticky ancestor and changed a measured desktop behaviour for no reason.

   EVERY MEASUREMENT QUOTED BELOW WAS TAKEN UNDER THE OLD CLASS NAME, on the old
   markup, and the prose says so rather than being rewritten to look current. The
   numbers are the record; renaming them would forge it. What that also means, and
   it is the honest caveat on this whole release: a compensation whose cause was
   the old arrangement has been RE-POINTED, not RE-MEASURED. Two of them are worth
   naming because the arrangement genuinely changed underneath them:

     - the 44px pin and `align-items: flex-start` on the menu box were measured
       with the menu INLINE in a single wrapping bar row. Two-row gives the menu
       its own full-width row. Their cause — a closed drawer taking its natural
       box in normal flow — is independent of which row it sits in, so they are
       kept; that reasoning is not a measurement.
     - the mobile `flex: 1 0 100%` on the logo faked "logo on top, controls
       beneath" inside one row. Two-row does that structurally. It is kept for the
       brand row, which still holds four items on one line at 390px.

   Rules asserted, render owed — and these two are the first place to look.
   ------------------------------------------------------------------------- */

/* The skin only. THE STICKY IS NOT HERE AND MUST NOT BE PUT BACK HERE.
   It lived on this selector through 2.2.0 and did nothing: WordPress wraps
   the header template part in its own `<header class="wp-block-template-part">`,
   so this element's parent is exactly as tall as this element, and a sticky
   box with zero travel scrolls away like an ordinary one. Bones now sticks the
   wrapper (see upline-bones.css §4). Re-adding `position: sticky` here is not
   a second belt — it re-establishes the inner element as its own containing
   context and breaks the working one. */
/* Two bands, not one. The mockup runs a dark announce strip over a WHITE main
   bar; the previous build painted the whole header inverse, which is the single
   biggest reason it did not read as the design. `position: relative` here is
   load-bearing — see .rl-nav below, which hands the mega panel this element as
   its containing block so it can span the viewport. */
.rl-header {
	position: relative;
	background-color: var(--wp--preset--color--base);
	color: var(--wp--preset--color--contrast);
}

.rl-header a {
	color: inherit;
}

.rl-announce {
	background-color: var(--wp--preset--color--inverse);
	color: var(--wp--preset--color--inverse-text);
	text-align: center;
	padding-block: var(--wp--preset--spacing--20);
}

/* `.rl-header` above is the ONE PAINTER for this header, and as of 2.15.6 that is a
   decision this file has to state rather than a fact it gets for free.

   Bones 2.3.29 paints `.ub-header` from `surface`, because the chassis makes the
   template-part wrapper sticky and range measured the whole stack transparent on bare
   Bones — scrolled content rendering through the bar. That fix is right and this skin
   asked for it. But `.ub-header` is the outermost group of every header pattern, so on
   Ridgeline the chassis's paint lands INSIDE `.rl-header` and on top of it: this theme's
   sheet depends on `upline-bones` and enqueues after it, so at equal specificity this
   rule wins, and without the declaration below the bar would come out `surface` where the
   approved comp has a WHITE bar under a dark announce strip. The chassis gaining a
   sensible default is exactly when a skin's silence stops being neutral.

   So the paint is handed back to `.rl-header` explicitly. Transparent rather than
   `base`: one painter is easier to reason about than two agreeing ones, and `.rl-header`
   has to paint anyway — it wraps the announce strip as well as the bar. Content cannot
   render through, because the opaque ancestor is `.rl-header` and it covers both.

   IF THIS SKIN EVER RENDERS A HEADER PART WITHOUT `.rl-header`, this declaration becomes
   the defect Bones just fixed, transplanted into the child. Gate 26 checks the invariant
   per artifact for that reason. */
.ub-header {
	background-color: transparent;
	border-bottom: 1px solid var(--wp--custom--upline--rule);
}

.rl-announce p {
	margin: 0;
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--mono-xs);
	letter-spacing: var(--wp--custom--rl--mono-track, 0.14em);
	text-transform: uppercase;
}

/* THE ANNOUNCEMENT SCROLLS AWAY AND ONLY THE BAR STICKS — 2.15.15, Ben's ruling.
   ------------------------------------------------------------------------
   The chassis sticks the header TEMPLATE PART, not the bar inside it (2.3.29's
   docblock explains why at length: an inner sticky's containing block is the
   wrapper, which is only as tall as its own content, so it has no travel). This
   skin puts the announcement inside that part, so the whole 122.9px stuck —
   announcement included — and everything below it had to clear all of it.

   SO THE PART STILL STICKS AND THE SHED IS A NEGATIVE OFFSET. Pulling the
   sticky top up by the announcement's own height parks it above the viewport
   while the bar holds at the top. Nothing moves in the resting layout; only the
   stuck position changes.

   THE SHED IS ANNOUNCEMENT **PLUS BLOCK GAP**, and the second term is the one
   this bench got wrong first. Shedding only the announcement (29.9) left the
   bar's top at y20, with a 20px TRANSPARENT BAND above it where `.rl-header`'s
   block gap sits — and page content scrolled through that band. Measured:

     shed 29.9   announce bottom 0     bar y20 → 93    20px of page showing
     shed 49.9   announce bottom −20   bar y0  → 73    flush ✓

   The gap is `--wp--style--block-gap`, which this theme's own theme.json sets
   to `spacing--60` — so it is read, not assumed to be 20.

   THE PIN, AND ITS CAVEAT, WHICH IS GATE 28'S. The shed needs the
   announcement's height as a number, so the height is pinned rather than left
   content-driven — a reservation that measures nothing is the defect gate 28
   exists for, and an unpinned one here would silently stop matching. The cost
   is the other side of that same rule: **copy longer than one line will
   overflow this box rather than grow it.** One line of mono at this size is
   what the design is, the box is the skin's own content rather than a shop's
   free text, and an overflowing announcement is visible where a drifting shed
   would not be. If the announcement ever becomes shop-editable, this pin is the
   first thing that has to go.

   AND NO `overflow: hidden`, WHICH THIS RULE BRIEFLY HAD. Clipping the second
   line would protect the shed's arithmetic by hiding the evidence — a silent
   failure in place of a visible one, which is the exact trade the bottom bar's
   pin refused one release ago ("a visible defect in the bar rather than an
   invisible one in the page"). Spilling text is ugly and findable; a clipped
   word is neither. */
.rl-announce {
	block-size: var(--wp--custom--rl--announce-h, 30px);
}

.wp-site-blocks > header.wp-block-template-part {
	top: calc(
		var(--ub-sticky-top, 0px)
		- var(--wp--custom--rl--announce-h, 30px)
		- var(--wp--style--block-gap, var(--wp--preset--spacing--60))
	);
}

/* THE SKIN'S OWN TWO HEIGHTS ARE GONE — 2.15.15, Fault C ruled (a).
   ------------------------------------------------------------------------
   This rule used to read:

       .ub-collapse {
           --ub-collapse-from: var(--wp--custom--rl--header-h, 64px);
           --ub-collapse-to: var(--wp--custom--rl--header-h-stuck, 52px);
       }

   and the paragraph above it argued that the two numbers were the skin's whole
   job here — "NOT the 72/44 the chassis defaults to … because this is
   Ridgeline's design" — and closed by predicting they were "the one thing in
   this release most likely to want moving". That prediction was right, and the
   argument was wrong in a way worth keeping rather than deleting:

     - 64 DID NOT DESCRIBE THIS BAR. The bar rests at 72 (44 of tap-sized
       controls + 28 of brand-row padding), so `--from: 64` was 8px under its
       own header. The outer `.ub-collapse` reserves `--from`, so the page
       height moved 8px during a collapse — the exact feedback the chassis
       primitive's docblock says the design prevents. Measured: resting outer
       72, condensed outer 64.
     - 52 WAS NOBODY'S DRAWING. The chassis default 44 is 2a's spec exactly,
       and 44 is also the floor the mechanism can reach, because the content is
       tap-sized controls. Measured four ways in the Fault B record.

   So both numbers are deleted rather than corrected, and the chassis defaults
   stand: 72 → 44. `--wp--custom--rl--header-h` and `--rl--header-h-stuck` are
   removed from theme.json with them — the first had no consumer left once this
   rule went, and the second lost its last one when the buy column's offset
   moved to the chassis token (see the PDP note further down). A token nobody
   reads is the figure-nobody-owns-bumping problem in its smallest form, and
   this file has now deleted three of them for the same reason.

   WHY THEY ARE RESTATED ON `body` BELOW RATHER THAN SIMPLY DROPPED. The
   chassis declares both on `.ub-collapse`, and custom properties inherit
   downward only — measured on bikeco's PDP, `--ub-collapse-to` reads EMPTY on
   `body` and on `.rl-product__buy`, while a `settings.custom` token reads
   everywhere because WordPress emits those at root. So nothing outside the
   header can see the chassis's copy, and this skin has a sticky sibling in the
   page that needs the number. Restating at body level is what makes it
   readable there; `body.upline-header--<option>` is the site because the
   plugin emits that class for every option from 1.56.0 and it is a designated
   hook, which it was not when these tokens were first written.

   The values are the chassis defaults on purpose — this is a RELAY of the
   chassis number into a scope the page can read, not a second opinion about
   it. When the per-option split arrives (2b at 42 and 2e at 40 are withdrawn
   for now: both are below the published tap floor, see the Fault C record)
   this is where it lands, one option per line.

   BETTER HOME, NOT TAKEN HERE: the chassis could declare these at body level
   itself, and then every skin with a sticky sibling would get them free
   instead of each restating them. Raised for Bones rather than done here,
   because this cut was scoped as skin-only. */
body.upline-header--drawer,
body.upline-header--utility-line {
	--ub-collapse-from: 72px;
	--ub-collapse-to: 44px;
}

/* TWO-ROW RESTS AT ITS OWN FLOOR — 2.15.16, Ben's call on bikeco's header.
   ----------------------------------------------------------------------
   Asked for: logo hard left, search over beside the account cluster, and the
   space between the logo row and the menu row cut so the header is about half
   its height. It was 165 at 1440.

   THE STRIP CANNOT GO BELOW 44 AND SO IT CANNOT COLLAPSE. The strip is this
   option's `ub-collapse__bar`, and the thing inside it is the plugin's menu,
   whose links carry `min-height: var(--upline-menu-bar-h)` — 44, the contract's
   `heights.bar`, measured not assumed. Resting the strip at 44 therefore puts
   it ON its content floor, and `--ub-collapse-to` cannot be lower than a tap
   target. So `from` and `to` are the same number here, and **two-row loses
   condense-on-scroll at desk** — there is nothing left to condense.

   That is a real trade and it is Ben's to keep or reverse: the alternative is
   resting higher (say 56 -> 44) and keeping the condense, at the cost of not
   being half. Recorded here rather than decided quietly.

   THESE TOKENS ONLY STARTED WORKING AT BONES 2.3.42. Declared on `body` since
   2.15.15, they were shadowed the whole time by the chassis's own declaration
   on `.ub-collapse` — a property declared on an element beats the same
   property inherited from an ancestor. 2.3.42 turns those into `var()`
   fallbacks, which is what makes this rule reach anything at all. Installing
   this skin against 2.3.41 or earlier leaves the strip at 72 and the header at
   117 rather than 88. */
body.upline-header--two-row {
	--ub-collapse-from: 44px;
	--ub-collapse-to: 44px;
}

/* bikeco's two-row PHONE arrangement — Ben, 2026-10-07.
   ----------------------------------------------------------------------
   Asked for: the hamburger where the logo is, left aligned; the shop info
   centred; the logo centred on a row of its own ABOVE the menu/search/cart row.

   THIS NEEDS THE TWO CONTAINERS MERGED, and that is worth stating because it
   was a parked question. The hamburger lives in `.ub-header__strip`; search and
   actions live in `.ub-header__brand-row`. They are different parents, and CSS
   cannot move a box between parents — so putting the hamburger on the same row
   as search and cart means dissolving both wrappers with `display: contents`
   and gridding `.ub-collapse` itself. That is the "unify" measured at 2.3.39
   and rejected there, on the ground that `.ub-collapse__bar` stops being a box
   and the collapse contract changes.

   WHAT CHANGED IS NOT THE MEASUREMENT, IT IS THE CONTRACT. 2.15.16 set
   two-row's `--ub-collapse-from` and `--ub-collapse-to` to the same 44 at Ben's
   request, because the strip sits on its content floor. **Two-row has no
   collapse left to break.** The objection was never about the mechanism; it was
   about losing a behaviour, and that behaviour is already gone by choice. For
   the other options it still stands and the unify stays parked.

   SCOPE, STATED: this overrides the chassis's own phone grid for two-row
   (2.3.39). bikeco's phone header is therefore the skin's arrangement, not the
   chassis's, and will not follow a future chassis change to it. That is the
   cost of a per-shop look and it is Ben's to accept.

   Measured at 390: header 161 -> 135. Logo centred on 195 (the viewport's own
   centre), hamburger at 16, search and cart grouped right at the cluster's 20px
   gap — the same relationship as desk — contact centred on 195. No overflow. */
@media (max-width: 760px) {
	.ub-header--two-row .ub-header__brand-row,
	.ub-header--two-row .ub-header__brand-row > .wp-block-group,
	.ub-header--two-row .ub-header__strip,
	.ub-header--two-row .ub-header__strip > .wp-block-group {
		display: contents;
	}

	.ub-header--two-row .ub-collapse {
		display: grid;
		align-items: center;
		column-gap: var(--wp--style--block-gap);
		row-gap: var(--wp--preset--spacing--20);
		padding-inline: var(--wp--preset--spacing--50);
		padding-block: var(--wp--preset--spacing--20);
		grid-template-columns: max-content minmax(0, 1fr) max-content max-content;
		grid-template-areas:
			"logo    logo logo   logo"
			"menu    .    search actions"
			"contact contact contact contact";
	}

	.ub-header--two-row .ub-header__logo { grid-area: logo; justify-content: center; }
	.ub-header--two-row .ub-inline-menu { grid-area: menu; }
	.ub-header--two-row .ub-header__search { grid-area: search; }
	.ub-header--two-row .ub-header__actions { grid-area: actions; justify-content: flex-end; }

	.ub-header--two-row .ub-header__contact {
		grid-area: contact;
		justify-content: center;
		min-inline-size: 0;
	}

	.ub-header--two-row .ub-header__secondary { display: none; }

	/* THE DEAD SPACE AROUND THE MARK WAS A TAP TARGET, NOT PADDING — Ben asked
	   to halve it, 2026-10-07, and the cause is worth recording because it is
	   not where it looks.

	   Measured at 390 on an image logo: the mark is 17.5 tall (120 wide at
	   1858:271), but `.ub-header__logo a` carries `min-block-size: 44px` — the
	   tap floor — so the band came to 8 + 44 + 8 = 60, which is **3.43x the
	   mark**. None of that is padding anyone wrote; it is a 44px tappable area
	   wrapped around a 17.5px picture.

	   24px is the floor this drops to, and it is not a taste number: it is
	   WCAG 2.5.8's minimum target size. Band 60 -> 32.8, which is 1.87x the
	   mark and almost exactly the halving asked for.

	   TWO ALTERNATIVES WERE MEASURED AND NOT TAKEN. Removing the tap target
	   entirely reaches 1.73x — 0.14 better, for a logo link that is no longer a
	   reliable tap — and growing the mark to 30px reaches 1.67x with a 125px
	   header instead of 99.8. Both are one line away if Ben's eye prefers them.

	   The 4px is half of `spacing--20`, written as the division so it tracks
	   the scale rather than sitting outside it. */
	.ub-header--two-row .ub-header__logo a {
		min-block-size: 24px;
	}

	.ub-header--two-row .ub-collapse {
		padding-block: calc(var(--wp--preset--spacing--20) / 2);
		row-gap: calc(var(--wp--preset--spacing--20) / 2);
	}
}

/* bikeco's two-row desk arrangement — Ben, 2026-10-07.
   Ordering only: the row is already `nowrap` + `space-between` at desk, so the
   slots simply change places. `order` is not a function property, so this is
   appearance and stays the skin's side of gate 29. */
@media (min-width: 782px) {
	.ub-header--two-row .ub-header__logo { order: 0; }
	.ub-header--two-row .ub-header__contact { order: 1; }
	.ub-header--two-row .ub-header__search { order: 2; }
	.ub-header--two-row .ub-header__actions { order: 3; }

	/* the space between the logo row and the menu row */
	.ub-header--two-row .ub-header__brand-row { padding-block: 0; }
	.ub-header--two-row .ub-collapse > * + * { margin-block-start: 0; }

	/* SEARCH SITS AGAINST THE ACTIONS CLUSTER, AT THE CLUSTER'S OWN SPACING —
	   Ben, 2026-10-07. The gap between the account and the bag is the global
	   block gap (20px, measured on both), so this names THAT TOKEN rather than
	   the number: if the cluster's spacing ever changes, the search follows it
	   instead of drifting away from it.

	   The row was `space-between`, which is what put 275px between the search
	   and the account. `flex-start` plus one auto margin pair does both jobs at
	   once: every gap becomes the cluster's gap, and the free space collects
	   around the contact — which centres it between the logo and the search
	   without a single hard number. */
	.ub-header--two-row .ub-header__brand-row > .wp-block-group {
		justify-content: flex-start;
		column-gap: var(--wp--style--block-gap);
	}

	.ub-header--two-row .ub-header__contact {
		margin-inline: auto;
	}
}

/* The wordmark. Heavy, caps, tight — the mockup's "RIDGELINE." set solid.
   NOT adding the accent-coloured full stop as a pseudo-element: the mockup's
   brand happens to end in one, and so does bikeco's site title ("bike co."),
   which would render "bike co..". A shop whose title has no full stop and
   wants one adds it to the title. */
.ub-header__logo a {
	text-decoration: none;
	font-weight: 800;
	text-transform: uppercase;
	letter-spacing: -0.01em;
}

/* The nav, and the one structural selector in this file.
   ------------------------------------------------------------------------
   The menu block renders `nav.upline-menu--mega` with `position: relative`,
   which makes the nav itself the containing block for the absolutely
   positioned mega panel. That is fine while the menu occupies its own
   full-width row — which is where the previous build had to put it — but the
   mockup puts the top-level items INLINE in the bar, and an inline nav is a
   narrow flex item. Measured: moving the menu into the bar with the nav still
   relative collapsed the panel from 1480px to 202px.

   De-positioning the nav hands the panel up to `.rl-header` instead, which is
   full-bleed. Measured after: panel absolute, 1728px wide, left 0 — full
   viewport, wider than it managed in its own row.

   `\.rl-nav nav` names no plugin class; it is my own wrapper plus an element
   name. It is still structural rather than a state hook, so it is the most
   fragile line in this file: if the menu block ever wraps its nav in another
   element, or drops `position: relative`, this needs re-measuring. The durable
   fix is a plugin-side choice about what owns the panel's containing block,
   and that is worth raising rather than defending this selector forever. */
/* The bar row, mobile first and actually mobile this time.
   ------------------------------------------------------------------------
   Measured at a true 390px before this rule existed: the row ran the desktop
   arrangement unchanged — `nowrap`, 26px gaps, space-between — and the result
   was that `.ub-header__actions` measured 68->434 inside a 358px row on a 390px
   viewport. The mini-cart sat at 378->434: off the right edge, clipped rather
   than scrollable, so it was not merely ugly, it was GONE. The logo and the
   nav were both crushed to `width: 0` by the same overflow, and the menu
   toggle started at x=-5, off the left edge.

   Ben's mobile layout is logo on top, controls beneath it. That is one line of
   flex-wrap plus an order, so the same markup serves both breakpoints and no
   second header part is needed. */
/* `padding-block` IS GONE FROM THIS RULE, and its absence is a finding rather
   than a tidy-up. It used to sit here because `.rl-bar__row` WAS the row and
   nothing above it had padding. The chassis pattern puts `padding-top` and
   `padding-bottom` as an INLINE STYLE on `.ub-header__brand-row` — the parent —
   so keeping 30 here would have stacked on the pattern's 40 and made the row
   taller than either theme asked for.

   The part worth routing: an inline style cannot be overridden by a skin's
   stylesheet at all, short of `!important`, which gate 6 forbids. So the header
   patterns' spacing is currently the CHASSIS's decision and not the skin's,
   which contradicts the chassis being "dumb on purpose" about look. The fix is
   in Bones — block-supports padding on the pattern instead of a literal inline
   style, or no padding there and the skin supplies it — and it is out of this
   cut. Until then Ridgeline's header rows carry Bones' 40 rather than its own 30.

   The GAPS stay, because they are the skin's design and the chassis's blockGap is
   a generated `.wp-container-*` class of equal specificity — so source order
   decides, and this needs a rendered read rather than an argument. First item on
   the render list beside the collapse heights. */
.ub-header__brand-row > div,
.ub-header__strip > div {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	row-gap: var(--wp--preset--spacing--30);
	column-gap: var(--wp--preset--spacing--40);
}

/* THE 761-781 BAND — why this rule is in two pieces (v4, 2026-10-07).
   -------------------------------------------------------------------
   Bones' phone grids stop at `max-width: 760px`. This skin's desk arm starts
   at `min-width: 782px`. The contract publishes its boundary at **761**. So
   between 761 and 781 there is no grid and no desk override, and this rule
   used to apply there alone, unscoped, to EVERY header option — including the
   ones whose row it was never written for. Measured at 770 on the shipped
   sheets: 2f's logo 738 and its **contact 0** — the exact collapse that Bones
   2.3.37/38 fixed, alive in a 21px band because that fix was scoped to 760.

   THREE EARLIER FIXES FAILED, AND THE REASONS ARE THE RULE:

   v1 scoped the selector and broke desktop. Scoping raised specificity from
   (0,1,0) to (0,3,0) while the desk override stayed (0,1,0), so the scoped
   rule WON at >=782 — at 800, 2b's logo went to 768 and its contact to 0.
   *A rule made more specific in order to apply in fewer places also applies
   more strongly in the places it still reaches.*

   v2 bounded it at `max-width: 781px`. Correct today — and it would have
   silently defeated the queued 782->761 alignment, since its (0,3,0) still
   out-specifies a desk arm moved to 761.

   v3 gave both rules the same selectors but added `order: 0` to the base, and
   that moved 2a's logo at 1440 from x 550.6 to 86: the desk arm overrides
   `flex` ONLY, so the desk bar relies on `order: 1` persisting from here.

   v4, below: same selectors on both, and the desk arm keeps overriding `flex`
   alone. The base carries what every option wants; the wrapping row's shape is
   scoped to the two containers that actually have that row. No media bound, so
   the 782->761 alignment needs no edit here. */
.ub-header__logo {
	flex: 0 1 auto;
	order: 0;
	justify-content: center;
	min-inline-size: 0;
}

.ub-header__brand-row > div > .ub-header__logo,
.ub-header__strip > div > .ub-header__logo {
	flex: 1 0 100%;
	order: 1;
}

/* The logo slot. Ridgeline shipped a site-TITLE here and no logo slot at all,
   which is why bikeco's header had been edited in the Site Editor to hold a
   site-logo: the shop had a logo and the theme offered nowhere to put it, so
   the editor was the only door open. The block reads the site's own Custom
   Logo option rather than naming a file, so it stays portable — no attachment
   URL in a theme part. A shop with no logo set renders nothing here until it
   sets one, which is the honest degradation.

   2.15.10: SCOPED TO THE DESK, BECAUSE THIS RULE WAS QUIETLY OVERRULING THE
   CHASSIS. `display: block` stays everywhere — that is dressing. The other
   three move behind `min-width: 761px`, and the reason is worth the space.

   The mark's 150.8px width was never a width. It is `block-size: 22px` times
   the source's 1858/271 ratio — a HEIGHT pin seen sideways — and beside it
   `max-inline-size: none` opts the mark out of any constraint at all. Below the
   boundary that combination is what let the logo overflow its column: Bones
   2.3.33 states the invariant that a logo never overflows its slot at <761, and
   this rule beat it. Not on specificity — they are equal — but on LOAD ORDER,
   because a child's stylesheet comes after its parent's. Measured, not assumed:
   the chassis rule injected at parent position computed `max-inline-size: none`
   and the mark stayed 150.8 in a 122px slot.

   Bones could have out-specified and won. That is the silent-cascade move §16
   exists to forbid, and the bench that keeps writing comments against it does
   not get to ship it. So the skin concedes instead: the 22px pin was always a
   DESK decision, it says so now, and the chassis invariant below the boundary
   is uncontested rather than victorious.

   Nothing about the desk render changes — the pin and the ratio are identical
   at >=761, where every pixel of this skin's header was measured. */
.ub-header__logo img,
.ub-header__logo .custom-logo {
	display: block;
}

@media (min-width: 761px) {
	.ub-header__logo img,
	.ub-header__logo .custom-logo {
		block-size: 22px;
		inline-size: auto;
		max-inline-size: none;
	}
}

/* The nav's mobile footprint, and why it is pinned rather than left to size.
   ------------------------------------------------------------------------
   Measured at 390px on 2.9.0: `.rl-nav` came out **661px wide** — 16 to 677 on
   a 390px viewport — because the closed menu bar inside it still takes its
   natural desktop width, even at zero height. With the row set to wrap, a
   661px item cannot share a line with anything, so `.ub-header__actions` was pushed to
   a third row: hamburger centred at y=160, icons at y=222. That is the
   "hamburger sits higher than the icon row" report, and the cause is width,
   not vertical alignment.

   Pinning the nav to one tap square puts the toggle back on the icon row, and
   pinning its HEIGHT stops an opening drawer from inflating the row and
   shoving the icons down the page — which is the symptom half of "the drawer
   opens in the middle of the header". `overflow: visible` lets the drawer
   spill below the bar instead of pushing it apart.

   What this does NOT do is give the drawer a proper full-width panel under the
   header band. The drawer is `.upline-menu__bar`, an internal class this file
   is not allowed to name, so its width and placement are the plugin's — and
   they arrive with the three-pane rebuild. This is the frame, not the
   interior. */
/* `align-items: flex-start` is the outage fix — 2.11.3, and it is load-bearing.
   ------------------------------------------------------------------------
   This box is 44px tall and `display: flex`. Under 1.23.0 its child
   `nav.upline-menu--mega.upline-menu--panes` measures 550.4px even CLOSED,
   because the new panes drawer computes `grid-template-rows: 506.398px` with
   `visibility: hidden; opacity: 0` — hidden, but not collapsed, so it still
   takes its full height in flow.

   `align-items: center` then centres a 550.4px child in a 44px box, which
   splits the overflow EVENLY: (550.4 - 44) / 2 = 253.2px above the box as well
   as below. Measured live at 390px: `.rl-nav` at y=137.9, inner nav at
   y=-115.3, and the toggle — the only control that opens or closes the menu —
   rendered 115px ABOVE the top of the viewport. Not hidden, not mispositioned:
   off-screen, unreachable, with no scroll that reaches it. That is the "no
   mobile navigation at all" report, and the hard-restart lockout before it.

   `flex-start` sends the whole overflow downward. Measured, same page, same
   instant: toggle back to y=137.9, `.ub-header__actions` unmoved at y=137.9, row height
   unchanged at 96px. Nothing the 44px pin was protecting is given up.

   Three other candidates were measured and rejected in the same pass, which is
   why this rule is one word and not a rewrite:
     - `block-size: auto` fixes the toggle and costs `.ub-header__actions`, which drops
       from y=137.9 to y=391.1 — the icons-shoved-down-the-page regression the
       pin exists to prevent. Same for `auto` plus `flex-start` together.
     - restoring `position: relative` on the inner nav changes nothing here
       (identical geometry), so it is NOT bundled: it is a desktop question,
       it is not causing the outage, and an outage fix carries one decision.

   The deeper point for whoever reads this next: a hidden element that is hidden
   WITHOUT being collapsed still has a height, and a flex container will happily
   centre that height around itself. `visibility: hidden` and `opacity: 0` say
   nothing about layout. Check the box, not the appearance. */
.ub-inline-menu {
	flex: 0 0 auto;
	order: 2;
	min-inline-size: 0;
	inline-size: var(--wp--custom--upline--tap);
	block-size: var(--wp--custom--upline--tap);
	position: relative;
	overflow: visible;
	display: flex;
	align-items: flex-start;
	justify-content: flex-start;
}

/* `.rl-nav nav` — the width compensation is GONE, and this is its epitaph.
   ------------------------------------------------------------------------
   From 2.11.4 this rule carried `inline-size: calc(100vw - 32px)` and a
   matching `max-inline-size`, because the drawer was in normal flow inside
   `.rl-nav`'s 44px tap square and inherited its width: the panes computed 30%
   of 93px and rendered as a narrow strip. The comment then said it "should come
   out the day the drawer stops depending on its ancestor for width."

   That day was upline-facets 1.25.0, which took the box outright. Measured on
   the live stack at 390px, plugin asset 1.26.0, drawer opened by real click:
   `position: fixed`, inset 0, **390 x 844** — the whole viewport — z-index 100.
   And the deletion was proven before it was made: with the compensation
   simulated away, every drawer number was BYTE-IDENTICAL. A fixed element does
   not inherit its ancestor's width, so the rule had stopped doing anything.

   `position: static` STAYS, and its reason is unrelated to the above. It is
   desktop-only load-bearing: the mega panel resolves against `.rl-nav`, and
   restoring the plugin's `relative` collapses the panel from 866px to 309px at
   1280. Measured twice — inert at mobile, decisive at desktop. Both of its
   "inert" readings were taken in the mobile arm, which is why it was never
   deleted on them: a remedy validated against the wrong world. */
.ub-inline-menu nav {
	position: static;
}

/* The menu toggle — the file's only plugin-class selectors, and they are here
   by designation, not by reaching.
   ------------------------------------------------------------------------
   `.upline-menu__toggle` and `.upline-menu__toggle-icon` are public stylable
   hooks under the build spec's "style the label, never replace the mechanism".
   Gate 6 carves out exactly these two names and nothing else; any other
   `.upline-*` selector in this file is still a gate failure.

   Ben's call: a plain three-line hamburger, no "Shop" wordmark beside it. The
   plugin already DRAWS the three lines — measured in its shipped
   components.css, `1.1em` wide bars on the icon span plus its two
   pseudo-elements — so nothing is redrawn here. What is removed is the button
   chrome (border, radius, padding, gap) that made it read as a boxed control
   rather than an icon.

   2.8.0 removed a workaround rather than adding one. 2.7.0 hid the word by
   zeroing type on the label and restoring it on the icon span, because
   emptying the block's `title` then left the control with no accessible name
   at all — the icon span is `aria-hidden` — and an icon-only mode that kept a
   name was routed to the plugin instead. 1.21.0 shipped it: an empty title now
   swaps the visible text for visually-hidden text and marks the control
   `--icon`. So the title is empty in the header part, the font-size trick is
   gone, and the hidden text is left alone — the tokens doc is explicit that it
   is not the theme's to remove. */
.upline-menu__toggle {
	gap: 0;
	padding: 0;
	border: 0;
	border-radius: 0;
	justify-content: center;
	min-inline-size: var(--wp--custom--upline--tap);
	min-block-size: var(--wp--custom--upline--tap);
}

/* The collapsed search, ALSO no longer faked. 2.7.0 shipped the field sized to
   fit and routed the magnifier, because the block had no icon mode and this
   file's own older note said collapsing-and-expanding is behaviour the search
   block owns. 1.21.0's `collapsible` is that behaviour, and it degrades the
   right way round: the form renders expanded with its toggle hidden and the
   script collapses it, so a shop with no JavaScript keeps a working search
   rather than a button that cannot open.

   Both toggles get the same treatment — bare glyph, tap-sized, no button
   chrome — so the magnifier and the hamburger read as one row of controls
   rather than two different kinds of thing. */
/* DISPLAY IS NOT THIS SKIN'S TO SET ANY MORE — 2.15.13, and it was overriding
   the plugin from outside without anybody choosing to.
   ------------------------------------------------------------------------
   `display: inline-flex` used to be here. From 1.56.0 the plugin decides this
   control's display PER HEADER OPTION — `@media (min-width: 761px)` with
   `body.upline-header--drawer|two-row|push|utility-line … { display: none }`,
   because those four show a field at desk and the magnifier is redundant there.
   That rule is inside `@layer upline`, this file is unlayered, and an unlayered
   normal declaration beats a layered one — so the skin's `inline-flex` won and
   the toggle kept rendering at 44x44 beside a visible 416px field at 1440.
   Measured on bikeco, logged out, 1440: field visible at 251, toggle still
   there. Nobody decided that; the cascade did, which is the silent-override
   failure this file has now produced three times in three different places.

   NOTHING REPLACES IT, and that is the point worth recording. The plugin's own
   base rule already declares `display: inline-flex; align-items: center;
   justify-content: center` on this control, so removing the skin's copy leaves
   a centred flex box at every width rather than a `<button>` fallen back to its
   UA default. The contingency in the cut order — "if alignment needs a display
   value put it on the slot, not the plugin's control" — therefore does not
   fire: there is nothing to put anywhere.

   WHAT STAYS IS APPEARANCE, which is the seam. `border: 0` and
   `border-radius: 0` are the T0 suppressions and are NOT redundant: this
   control is a `<button>`, so Bones' `:where(button:not(…)) { border: 1px
   solid }` matches it where it never matched the menu's two `<label>`s. The
   two tap minimums below are a different matter and are left alone pending a
   ruling: the plugin declares `min-width`/`min-height` with `!important`, and
   an important LAYERED declaration outranks an unlayered normal one — the
   inverse of the normal-declaration case above — so these two are most likely
   inert. Suspected, not proved, and not quietly deleted on a suspicion. */
.upline-search__toggle {
	padding: 0;
	border: 0;
	border-radius: 0;
	background: none;
	color: inherit;
	align-items: center;
	justify-content: center;
	min-inline-size: var(--wp--custom--upline--tap);
	min-block-size: var(--wp--custom--upline--tap);
}

/* The desktop mega panel's outer edge — removed, 2.11.9.
   ------------------------------------------------------------------------
   Ben ruled this one with his own hands: in DevTools he unchecked
   `border: 1px solid var(--upline-rule)` on `.upline-mega` and said that is
   the look — no outer edge on the desktop panel, not a fainter one. He
   unchecked a declaration; he did not recolour one, which is why setting the
   rule token to `transparent` was considered and declined. That dodge leaves
   1px of layout behind and fights the 2.11.5 un-pin besides.

   `.upline-mega` is a designated public surface as of plugin 1.26.1, and the
   designation is narrow in a way that matters: the PANEL'S BOX is the theme's
   to style; every `__*` name below it stays internal and moves without notice.
   This rule touches the box and nothing inside it.

   THE MEDIA QUERY IS NOT OPTIONAL. The same class does two opposite jobs, and
   both were re-measured on served 1.26.1 rather than carried over from the
   1.26.0 reading:

     1728  three `--brands` panels 0/1/1/1 and `--plain` 1/1/1/1 — the outer
           edge (brands panels already sit flush to the bar, hence top 0)
     390   all four panels 0/0/0/1 — LEFT ONLY, the drawer's pane separators,
           which Ben's drawings explicitly keep

   So a global `border-width: 0` would delete the separators. Proven both ways
   before shipping: with this rule injected live, desktop reads 0/0/0/0 on all
   four panels and the 390 frame still reads 0/0/0/1 on all four.

   THE BOUNDARY IS 761, CORRECTED AT 2.11.10, and cited against the plugin's
   published boundary table (tokens doc stamped 1.27.1, "The layout
   boundaries"), which names `min-width: 761px` as the menu surfaces' desktop
   boundary and states that using the rail's number for the menu's is "wrong by
   22px" — this rule's defect, named by the contract in the same words.

   It shipped at 782 — WORDPRESS's breakpoint and this file's own design
   breakpoint, inherited because it was the number already in hand. The
   correction was originally excavated from `components.css`; the table has
   since promised the same values, with a suite asserting them in both
   directions. Between 761 and 781 the plugin
   is therefore in DESKTOP layout while a 782-scoped rule has not started, so
   the outer edge this rule exists to delete was still drawn in a 21px band.

   Found while fixing the close-icon defect in Bones 2.3.10, which was the same
   mistake with a louder symptom. **A rule that governs the plugin's surface
   takes the plugin's breakpoint, not ours** — the numbers look
   interchangeable and are not, and this file's other five `782px` queries are
   Ridgeline's own design breakpoint and stay exactly where they are. */
@media (min-width: 761px) {
	.upline-mega {
		border-width: 0;
	}
}

.ub-header__actions {
	flex: 1 1 auto;
	order: 3;
	justify-content: flex-end;
	min-inline-size: 0;
}

/* One row, one optical weight, one baseline.
   ------------------------------------------------------------------------
   The three header controls come from three different owners — the magnifier
   is the plugin's collapsible-search toggle, the account link and the cart are
   Woo blocks — and each arrived at its own size. Measured at 390px: search
   toggle 44px tall, cart 44px, account **59px**. A 15px discrepancy in a row
   of three icons is exactly what reads as "from three families" even before
   the glyphs are considered.

   2.7.0 set a `min-block-size` here, which is a floor and not a ceiling, so
   the block kept its 59px. This pins the box instead, on the wrapper and on
   the link inside it, and gives every control the same tap square so the three
   share a centre line.

   The GLYPHS are a separate problem with a different home. `iconStyle: "line"`
   on the customer-account block — read off its block.json, not guessed — turns
   the filled person into an outline one without any CSS. The mini-cart has no
   equivalent: its only icon attribute is `miniCartIcon` (cart / bag variants,
   all filled). Replacing a vendor glyph outright means shipping an icon set,
   and an icon set that only Ridgeline can use is the exact thing that should
   live in the chassis — so it is raised for Bones rather than drawn here. */
/* THE MAGNIFIER LEFT THIS SELECTOR LIST AT 2.15.13, and the three vendor
   controls keep every declaration in it.
   ------------------------------------------------------------------------
   The cut order said "drop `display` from both Ridgeline rules". Taken
   literally here that would strip `display: flex` from the account block, the
   account link and the mini-cart button as well — the three controls this rule
   exists for, whose 59px-against-44px mismatch the paragraph above spends ten
   lines on. So the DIVERGENCE is deliberate and small: the toggle leaves the
   selector instead, which gives the toggle no display from this skin while the
   rule goes on doing its job for the three elements that are not the plugin's.

   The toggle loses this rule's `block-size` pin too, and that is the one thing
   worth checking rather than assuming. The pin was added because
   `min-block-size` is a floor and the account block sailed past it at 59px. The
   magnifier never did: the plugin gives it `min-height: var(--upline-tap)` with
   0 padding and a 1.1em glyph, so its content cannot push it over 44 and the
   floor is also the height. Measured after the cut, not reasoned: 44 at 390. */
.ub-header__actions .wp-block-woocommerce-customer-account,
.ub-header__actions .wc-block-customer-account__link,
.ub-header__actions .wc-block-mini-cart__button {
	block-size: var(--wp--custom--upline--tap);
	min-block-size: var(--wp--custom--upline--tap);
	display: flex;
	align-items: center;
	justify-content: center;
}

/* The vendor glyphs are NOT sized here any more, and the removed rule is worth
   a note because it caused the bug it was written to prevent.

   2.10.0 set `inline-size: 20px; display: block` on the account and cart
   `<svg>`s to even out three mismatched icons. Bones 2.3.0 then took ownership
   of those glyphs and hid them in favour of the icon slots — but this rule was
   two classes deep (`.ub-header__actions .wc-block-…__link svg`) and the chassis rule
   was one, so the SKIN won and un-hid the very thing the chassis was
   replacing. Every control rendered twice: the theme's slot glyph beside the
   vendor's own.

   The seam lesson, which is the reusable part: when a rule moves up to the
   chassis, the skin's old rule does not become redundant — it becomes an
   active opponent, and a more specific one, because skins are written closer
   to the element than chassis rules are. Moving a rule up means deleting it
   down, in the same pair of releases.

   The sharp part of this one is that the habit already existed and simply did
   not fire twice. The SAME shipment repatriated the inline-menu stretch rules
   correctly — they moved to Bones section 10 and `.rl-nav` gained
   `ub-inline-menu` in the header part, up and down in one pair. The icon hide
   was the identical move with only its first half done. So the guard is not
   "learn the rule"; it is to treat "what moves up" as a LIST for a release and
   walk it, because doing it right once in a shipment is no evidence at all
   about the second instance. */

@media (min-width: 782px) {
	/* `padding-block: 0` removed with its mobile counterpart above — it was
	   zeroing a padding this rule no longer sets, and the padding that IS on the
	   row is the pattern's inline style, which this cannot reach. A declaration
	   that cancels nothing is worse than absent: it reads as the desktop arm
	   controlling the row's height when it does not. */
	.ub-header__brand-row > div,
	.ub-header__strip > div {
		flex-wrap: nowrap;
		column-gap: var(--wp--preset--spacing--70);
	}

	/* Pairs with the band fix: same selectors as the scoped base rule, and it
	   overrides `flex` only — `order: 1` must keep coming from that rule or the
	   desk bar loses its logo position (v3's defect). */
	.ub-header__brand-row > div > .ub-header__logo,
	.ub-header__strip > div > .ub-header__logo {
		flex: 0 0 auto;
	}

	/* Left-aligned beside the wordmark, not floating mid-bar.
	   ---------------------------------------------------------------
	   Measured on a 1728px viewport: the menu bar sat 724->976, dead
	   centre, with the logo at the far left and the icons at the far
	   right — three mono items marooned in the middle of the bar. The
	   mega panel then drops full-bleed from 0, so a centred trigger
	   opens a panel with no edge in common with it, which is what
	   reads as opening "from the middle": the thing you pointed at and
	   the thing that appeared share no alignment.

	   This is a skin decision and stays in Ridgeline — a different
	   Upline theme may well want it centred, and nothing here is
	   structural. One line to flip if the call goes the other way. */
	.ub-inline-menu {
		flex: 1 1 auto;
		inline-size: auto;
		block-size: auto;
		overflow: visible;
		align-items: center;
		justify-content: flex-start;
	}

	.ub-header__actions {
		flex: 0 0 auto;
	}
}

/* The inline-menu hover bridge USED TO LIVE HERE. It moved to Bones in
   2.11.0 / Bones 2.3.0 — see `upline-bones.css` section 10, which carries the
   full measurement record and the known limit on the diagonal.

   It was structure wearing a skin's clothes: every Upline theme that puts the
   menu inline in the bar needs those rules identically, so a theme re-deriving
   them is re-deriving the chassis. `.rl-nav` now carries `ub-inline-menu` in
   the header part and gets them by inheritance. Nothing about the fix changed;
   what changed is that the next skin adds a class instead of reading a
   comment. */

.ub-inline-menu nav > div > ul > li {
	display: flex;
}

/* Top-level bar items only — mono caps, per the mockup. The mega panel's own
   links stay sentence case, which is why this is depth-scoped rather than a
   blanket `.rl-nav a`. Structural for the same reason as above: there is no
   menu typography token to reach for yet. */
.ub-inline-menu nav > div > ul > li > a {
	display: flex;
	align-items: center;
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--mono-sm);
	letter-spacing: var(--wp--custom--rl--mono-track, 0.14em);
	text-transform: uppercase;
	font-weight: 700;
	text-decoration: none;
}

/* Actions. Woo publishes `wc-block-*` for themes and keeps it stable, so these
   are fair game in a way `upline-facets` internals are not. */
.ub-header__actions .wc-block-mini-cart__button {
	background-color: var(--wp--custom--upline--accent);
	color: var(--wp--custom--rl--accent-fg, #ffffff);
	border: 0;
	border-radius: var(--wp--custom--upline--radius);
	padding: 0 var(--wp--preset--spacing--60);
	min-block-size: var(--wp--custom--upline--tap);
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--mono-sm);
	letter-spacing: var(--wp--custom--rl--mono-track, 0.14em);
	text-transform: uppercase;
}

.ub-header__actions .wc-block-customer-account__link {
	min-block-size: var(--wp--custom--upline--tap);
	display: inline-flex;
	align-items: center;
}

/* The search sits in the bar as a compact field rather than the boxed
   input-plus-button pair Woo and the plugin default to. The mockup shows a
   bare "SEARCH" affordance that expands; collapsing to a label and expanding on
   activation is behaviour the search block owns, not something to fake from
   the theme, so this is the honest half: quiet, sized to the bar, not a box.

   2.7.0: Ben asked for a magnifier that opens the field on tap. The block was
   read at the registry rather than guessed — `upline-facets/search` exposes
   `instant`, `label`, `placeholder`, `showButton` and nothing else, so there
   is no icon mode to switch on, and block attributes are not per-breakpoint
   anyway: one instance, one set of values. A CSS-only collapse would have to
   blank the submit button's "Search" text and second-guess the instant-results
   combobox from outside, which is this comment's original point and still
   true. So the directive is ROUTED, not faked, and what changed here is only
   the honest half: on mobile the field stops being a fixed 240px — which is
   what shoved the mini-cart off the viewport — and shrinks with the row.

   2.8.0: the block's `collapsible` attribute is now set, so on mobile this
   wrapper holds a magnifier rather than a field and takes its natural width
   like the other two controls. It still stretches on desktop, where the field
   is expanded and wants the room. */
.ub-header__search {
	flex: 0 0 auto;
	min-inline-size: 0;
	max-inline-size: none;
}

@media (min-width: 782px) {
	.ub-header__search {
		flex: 0 1 auto;
		max-inline-size: 15rem;
	}
}

/* THE SKIN'S OWN COMPACT ANIMATION IS GONE — 2.15.0, deleted not retargeted.
   ------------------------------------------------------------------------
   What stood here was `@keyframes rl-header-compact` on `.rl-bar__row`, driven
   by `animation-timeline: scroll()`. The scoping doc's §5 records what it
   actually did: registered, `pending: true` in `getAnimations()`, never ran,
   touched nothing. So there was no behaviour to preserve — retargeting it onto
   `.ub-collapse__bar` would have moved a rule whose only measured property is
   that it does nothing, and left two implementations of one idea in the tree.

   The chassis's `ub-collapse` carries the mechanism and this file now supplies
   only the two heights, above. That is the seam working as designed, and it is
   also the "moving a rule up means deleting it down, in the same pair of
   releases" lesson from the icon-hide regression earlier in this file — applied
   deliberately this time instead of learned again.

   WHAT IS NOT CLAIMED: that the collapse now works. The chassis primitive has
   the same browser floor and, more to the point, the same unproven history —
   §5's standing risk is that scroll-driven collapse has never been observed to
   run in this codebase at all. Moving to the chassis's copy consolidates the
   mechanism; it does not verify it. That is still owed, and it is out of this
   cut by ruling.

   -------------------------------------------------------------------------
   THE CONTACT SLOT, AND WHAT THIS FILE USED TO SAY ABOUT IT — 2.15.1
   -------------------------------------------------------------------------
   2.15.0 shipped a note here saying these two slots rendered the chassis's
   PLACEHOLDER COPY, and rules styling it "so the placeholders read as deliberate
   rather than broken". Both are now wrong, and the second was wrong when it was
   written: copy that reads as deliberate reads as deliberate TO A SHOPPER too,
   which makes styling it worse than leaving it looking broken. Bones 2.3.23
   removed the copy under term 3 — an empty editorial region collapses, it never
   speaks — and 2.3.24 placed `upline-facets/contact` in the slot, so the shop's
   own phone, address, hours, branch names and locator arrive from Base with
   nobody typing them into a theme.

   So the prose is replaced rather than amended, and the rules that styled
   paragraphs in those slots are DELETED rather than re-pointed. Nothing emits a
   bare `<p>` in there any more: the block emits its own designated elements, and
   a rule matching markup that no longer exists is a rule that lies about what
   this theme does. Deleting down what moved up, in the same pair of releases —
   the icon-hide lesson from earlier in this file, applied on purpose again.

   WHAT IS STYLED NOW, and every name is transcribed from the plugin's own
   "Public stylable surfaces" table at 1.50.0 (55355 B, digest attested in the
   contract's `tokensDoc`), not read off the markup: the container, the phone
   link, the address, the hours, the branch names, the locator link, and the
   secondary link list. Mono caps and muted, matching the announce strip's
   treatment, because this is the same register of information — small, factual,
   subordinate to the wordmark.

   `.upline-contact__links` IS NOW IN ITS OWN SLOT — corrected at 2.15.3. The
   2.15.1 note here said the links rendered "in the wrong place", beside the phone
   number, because `part` was unpublished and one instance had to serve both slots.
   1.51.0 published it; Bones 2.3.26 places the block twice, `part="contact"` in the
   contact slot and `part="links"` in the secondary slot. So the links are where the
   frames put them and this rule styles them there. The old note is replaced rather
   than amended — it described a compromise that no longer exists, and a comment
   that survives the thing it was explaining is how the next reader gets misled.

   NOT STYLED, deliberately: `.upline-contact__link`'s individual state. The
   chassis's panel-ink carve-outs already keep links legible with no skin, and
   this file's standing position is that it styles the control and never
   replaces the mechanism. */
.ub-header__contact,
.ub-header__secondary {
	min-inline-size: 0;
}

/* A WRAPPING ROW — right for four of the six options, and corrected below for the two
   it was wrong for. The brand-row and utility-line arrangements put contact on a
   horizontal line, which is what this is. */
.upline-contact {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: var(--wp--preset--spacing--20) var(--wp--preset--spacing--40);
	margin: 0;
}

.upline-contact__phone,
.upline-contact__address,
.upline-contact__hours,
.upline-contact__branches,
.upline-contact__locator {
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--mono-xs);
	letter-spacing: var(--wp--custom--rl--mono-track, 0.14em);
	text-transform: uppercase;
	color: var(--wp--custom--upline--muted);
}

.upline-contact__phone,
.upline-contact__locator {
	text-decoration: none;
}

.upline-contact__links {
	display: flex;
	flex-wrap: wrap;
	gap: var(--wp--preset--spacing--30);
	margin: 0;
	padding: 0;
	list-style: none;
}

/* -------------------------------------------------------------------------
   2b. Contact, in the two options the wrapping row was wrong for
   -------------------------------------------------------------------------
   THE LIMIT THIS REPLACES, in its own words: "`sidebar` stacks its whole header in a
   200px column and `bottom-bar` truncates its top bar, so in those two the same rule
   wraps to several lines instead — legible, and not necessarily what the frames draw."
   It was left as one rule because splitting it needed a way to tell the two placements
   apart, and there wasn't one: both instances rendered `.upline-contact` and nothing
   else, so a rule for the details row hit the links row too.

   WHAT MADE IT FIXABLE. Plugin 1.51.5 puts `data-upline-part="contact|links|both"` on
   the wrapper. Keyed on that, a rule can reach the details row and leave the links row
   alone — which is the whole reason the attribute was asked for. Confirmed live on
   range at Bones 2.3.27 / 1.51.5: `.upline-contact[data-upline-part="links"]` in
   `ub-header__secondary`, so the hook is in the DOM and not just in the source.

   `both` IS INCLUDED WITH `contact` EVERYWHERE BELOW, because an instance placed with
   no `part` renders the details AND the links — more content in the same box, never
   less. Reading `both` as a third arrangement to design separately would be inventing
   one; reading it as "contact, plus extra" is what it is.

   SCOPED ON THE THEME'S OWN OPTION CLASS, not on `body.upline-shell--sidebar`. The
   pattern writes `ub-header--sidebar` and `ub-header--bottom-bar` itself, so the scope
   is true whenever the pattern renders. The plugin's body class depends on the shop's
   shell setting being in the state the header expects, which is a second condition this
   arrangement does not actually need — and a rule that needs two facts to be true fails
   in a way that looks like a rendering bug rather than a settings mismatch.

   THE TWO OPTIONS WANT OPPOSITE THINGS, which is why this is two rules and not one.
   That only became clear from reading what each pattern declares for the slot. */

/* SIDEBAR: a 200px column, so the row becomes a column. Wrapping in 200px puts the
   break wherever the shop's address happens to run out of room — a different place per
   shop, and per branch count. Stacking puts it after every field, which is at least the
   same shape for everyone. Tighter gap because the fields are now siblings in a stack
   rather than items on a line, and the 40 step was horizontal breathing room. */
.ub-header--sidebar .upline-contact[data-upline-part='contact'],
.ub-header--sidebar .upline-contact[data-upline-part='both'] {
	flex-direction: column;
	gap: var(--wp--preset--spacing--20);
}

/* BOTTOM-BAR: one line, because the pattern already says so. `ub-header__contact` in
   `header-bottom-bar.php` declares `flexWrap: nowrap` on the slot — the strip is
   deliberately a single line — and the base `flex-wrap: wrap` above contradicts it from
   the inside. So this is not a new arrangement, it is the row finally doing what the
   pattern asked.

   `min-inline-size: 0` is the part that does the work: a nowrap flex item refuses to
   shrink below its content by default, and without this the row pushes the strip wider
   than the bar instead of fitting in it. */
.ub-header--bottom-bar .upline-contact[data-upline-part='contact'],
.ub-header--bottom-bar .upline-contact[data-upline-part='both'] {
	flex-wrap: nowrap;
	min-inline-size: 0;
}

/* And the fields truncate rather than overflow. THIS IS THE ONE DECISION HERE THAT IS
   A DECISION and it is flagged for Ben rather than buried: in a 52px top bar with the
   row on one line, something has to give, and the choices are an ellipsis on the long
   fields (this), a bar that grows, or dropping fields below a width. Truncation keeps
   every field present and readable until the space runs out, and it degrades per-field
   instead of rearranging the strip. Dropping fields would be choosing which of a shop's
   details matter least, which is not this bench's call to make silently. Phone and
   locator are excluded: they are short, and they are the two a shopper acts on. */
.ub-header--bottom-bar .upline-contact[data-upline-part='contact'] .upline-contact__address,
.ub-header--bottom-bar .upline-contact[data-upline-part='contact'] .upline-contact__hours,
.ub-header--bottom-bar .upline-contact[data-upline-part='contact'] .upline-contact__branches,
.ub-header--bottom-bar .upline-contact[data-upline-part='both'] .upline-contact__address,
.ub-header--bottom-bar .upline-contact[data-upline-part='both'] .upline-contact__hours,
.ub-header--bottom-bar .upline-contact[data-upline-part='both'] .upline-contact__branches {
	min-inline-size: 0;
	overflow: hidden;
	white-space: nowrap;
	text-overflow: ellipsis;
}

/* NOT TOUCHED IN EITHER OPTION: `[data-upline-part='links']`. The links row is a
   wrapping row in all six arrangements and nothing measured says otherwise. Leaving it
   out of these rules is the point of keying on the attribute — before 1.51.5 it would
   have been swept into both corrections with no way to exclude it. */

/* -------------------------------------------------------------------------
   3. Footer
   -------------------------------------------------------------------------
   The alignment problem this solves: the columns were laid out with flex and
   space-between, so with four unequal columns the gaps between them were all
   different widths and nothing lined up with the grid above. A grid with
   equal auto-fit tracks gives every column the same width and the same left
   edge as the content above it, at every viewport, with no breakpoint.
   ------------------------------------------------------------------------- */

.rl-footer {
	background-color: var(--wp--preset--color--inverse);
	color: var(--wp--preset--color--inverse-text);
}

.rl-footer a {
	color: inherit;
}

.rl-footer a:hover {
	color: var(--wp--custom--rl--accent-on-inverse);
}

.rl-footer__cols {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--wp--preset--spacing--90) var(--wp--preset--spacing--70);
}

@media (min-width: 640px) {
	.rl-footer__cols {
		grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
	}
}

/* The brand column reads as intro copy, so it takes a wider track than the
   link columns and a reading measure. */
@media (min-width: 1024px) {
	.rl-footer__cols {
		grid-template-columns: 1.6fr repeat(3, 1fr);
	}
}

.rl-footer__brand p {
	max-inline-size: 34ch;
	color: color-mix(in srgb, currentColor 72%, transparent);
	margin-block: var(--wp--preset--spacing--40) 0;
}

.rl-footer__col ul {
	list-style: none;
	margin: 0;
	padding: 0;
	display: grid;
	gap: var(--wp--preset--spacing--30);
}

.rl-footer__col h2,
.rl-footer__col .rl-mono {
	margin-block: 0 var(--wp--preset--spacing--50);
	color: color-mix(in srgb, currentColor 60%, transparent);
}

.rl-footer__base {
	margin-block-start: var(--wp--preset--spacing--100);
	padding-block-start: var(--wp--preset--spacing--60);
	border-top: 1px solid color-mix(in srgb, currentColor 16%, transparent);
	display: flex;
	flex-wrap: wrap;
	gap: var(--wp--preset--spacing--40);
	justify-content: space-between;
	color: color-mix(in srgb, currentColor 60%, transparent);
}


/* -------------------------------------------------------------------------
   4. Section rhythm
   -------------------------------------------------------------------------
   The mockup's sections breathe at 52px on mobile and 88px from tablet up.
   Applied by class so a pattern opts in, rather than as a blanket rule that
   every full-width group would then have to fight.
   ------------------------------------------------------------------------- */

.rl-section {
	padding-block: var(--wp--custom--rl--section-y, 52px);
}

@media (min-width: 782px) {
	.rl-section {
		padding-block: var(--wp--custom--rl--section-y-lg, 88px);
	}
}

.rl-section--surface {
	background-color: var(--wp--preset--color--surface);
}

.rl-section--inverse {
	background-color: var(--wp--preset--color--inverse);
	color: var(--wp--preset--color--inverse-text);
}

/* Section head: title left, a mono link right, sharing a baseline. */
.rl-sectionhead {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	justify-content: space-between;
	gap: var(--wp--preset--spacing--40);
	margin-block-end: var(--wp--preset--spacing--70);
}

.rl-sectionhead h2 {
	margin: 0;
}


/* -------------------------------------------------------------------------
   5. Cards and tiles
   -------------------------------------------------------------------------
   Outlined, never shadowed — the one shadow in the design is the search
   dropdown, and that is the plugin's. Ratios come from tokens so the plugin's
   tiles and the theme's own agree.
   ------------------------------------------------------------------------- */

.rl-card {
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--upline--radius);
	background-color: var(--wp--preset--color--base);
}

.rl-tile {
	position: relative;
	display: block;
	border: 1px solid var(--wp--preset--color--line);
	border-radius: var(--wp--custom--upline--radius);
	background-color: var(--wp--preset--color--surface);
	overflow: hidden;
}

.rl-tile--category { aspect-ratio: var(--wp--custom--upline--tile-ratio-category); }
.rl-tile--brand    { aspect-ratio: var(--wp--custom--upline--tile-ratio-brand); }
.rl-tile--featured { aspect-ratio: var(--wp--custom--upline--tile-ratio-featured); }

/* The brand tile's text state. The build spec calls this the majority case on
   real shops, not a fallback, so it is designed rather than left bare. */
.rl-tile--text {
	display: grid;
	place-items: center;
	text-align: center;
	padding: var(--wp--preset--spacing--50);
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--mono-sm);
	letter-spacing: var(--wp--custom--rl--mono-track, 0.14em);
	text-transform: uppercase;
	background-color: var(--wp--preset--color--base);
}


/* -------------------------------------------------------------------------
   6. Product cards
   -------------------------------------------------------------------------
   Woo renders the card; these rules give it Ridgeline's proportions. The
   image ratio is fixed so a lazily-loaded image never shifts the grid — the
   half of the loading story Bones leaves to the child, because ratios are a
   design decision.
   ------------------------------------------------------------------------- */

/* Split into two rules at 2.11.8, because the two subjects were never the same
   subject and writing them as one hid a defect in each half.

   THE BOX takes the ratio and the ground. THE IMAGE takes neither.

   The ratio: `aspect-ratio` on the image was dead code from the day it was
   written. Woo's Product Image block emits `aspect-ratio:1/1` as an INLINE
   STYLE on the tag, and an inline style beats every rule in every stylesheet
   short of `!important`, which gate 6 forbids. So the box has been 4:5 and the
   image 1:1 the whole time, with 75px of dead ground under every card — a rule
   that looked like it worked and never did. It is deleted rather than left to
   read as intent. Bones centres the shorter image in the taller box, which is
   the honest way to absorb a ratio the theme does not control.

   The ground: painting the IMAGE with the tile colour duplicates a surface the
   box already provides — harmless until the blend below, at which point the
   image's own copy of 242 multiplies with the box's 242 behind it and every
   transparent product photo gains a 230 square that stops exactly at the image
   box. Measured, not predicted. The box keeps the ground; the image stops
   carrying a second copy of it. */
.wc-block-components-product-image {
	aspect-ratio: var(--wp--custom--rl--product-ratio, 4 / 5);
	inline-size: 100%;
	border-radius: var(--wp--custom--upline--radius);
	background-color: var(--wp--preset--color--surface);
}

/* `multiply` is a SKIN decision and stays here, not in Bones, because it is
   only correct over a light ground — and the ground is the skin's.

   The catalogue is mixed and always will be: measured on bikeco's apparel
   shelf, three of eight product photos are white-background JPEGs and five are
   transparent WebPs. The JPEGs show a white rectangle inside a 242 tile. Over
   this ground multiply is a uniform x0.9490 — pure white lands on exactly 242
   and the rectangle dissolves, and every other pixel darkens by 5.1%. Sampled
   from the source files, the largest shift anywhere in the set is 12 levels per
   channel, on the Giro Aries pastels; the dark products move 1 to 7. The shift
   is biggest exactly where it matters least.

   A skin on a WHITE tile ground inherits a mathematical no-op (x1.0) and loses
   nothing by not overriding this. A skin on a DARK ground must override it:
   multiply toward black crushes every photo. That conditionality is the whole
   reason this declaration is not in the chassis. */
.wc-block-components-product-image img {
	inline-size: 100%;
	object-fit: cover;
	border-radius: var(--wp--custom--upline--radius);
	mix-blend-mode: multiply;
}

.wc-block-grid__product-title,
.wp-block-post-title {
	letter-spacing: -0.01em;
}

/* Editorial imagery — journal cards and anything else on the long-form side.
   The ratio lives in the token, not in the block's `aspectRatio` attribute:
   that attribute emits an inline style, which no stylesheet rule can reach, so
   a pattern carrying it silently forks the ratio away from the token that was
   minted to hold it. Gate 14's reverse direction is what caught the fork. */
.rl-editorial-img,
.rl-editorial-img img {
	aspect-ratio: var(--wp--custom--rl--editorial-ratio, 3 / 2);
	inline-size: 100%;
	object-fit: cover;
	background-color: var(--wp--preset--color--surface);
}

/* NO BUY BUTTON ON A TILE — 2.11.11.
   The grid sells the browse; the product page sells the product. A 197x54
   accent slab under every tile makes a shelf read as sixteen calls to action
   competing with each other, and on a catalogue that is 15/16 variable the
   button is not even a buy control — "Select options" navigates to the product
   page, which is what the image and the title already do. Measured on bikeco's
   /collection/new/: 16 tiles, 15 product_type_variable, 1 simple.

   THIS IS THE ONE KIND OF RULE THE SKIN OWES AN ARGUMENT FOR.
   Setting `display` on a control another party renders is a contract question,
   not a styling one, so the cost was put to the owner before it was written:
   on a SIMPLE product this removes the only in-tile add-to-cart. Answered —
   all of them, everywhere a tile appears. Recorded here rather than in a commit
   message, because the next person to read this file is the one who needs it.
   What survives is the route: measured after, every tile still links to its
   product page and keeps its image, title and price; the tile shortens 619 ->
   566. Nothing becomes unreachable, and the product page's own add-to-cart is
   untouched (49px before and after).

   TWO RENDERERS, NOT ONE, AND THEY DO NOT SHARE A CLASS.
   The block loop (`product-collection` -> `product-template`) emits
   `.wp-block-woocommerce-product-button`. The classic loop — related products,
   up-sells, `[products]` — emits `ul.products li.product a.button` from PHP.
   A rule aimed at the block class alone leaves the related-products button
   standing, which is precisely the one that prompted this.

   THE `.woocommerce` LINE IS NOT DUPLICATION. `woocommerce.css` ships
   `.woocommerce ul.products li.product .button { display: inline-block }` at
   (0,4,2). The bare selector below is (0,3,3) and LOSES on class count — tested
   first in that form, and the related button did not move. The scoped line ties
   at four classes and wins on elements. Both are kept: Woo's rule only applies
   under a `.woocommerce` ancestor, and where that ancestor is absent so is the
   thing being outranked. */
ul.products li.product a.button,
.woocommerce ul.products li.product a.button,
.wc-block-grid__products .wc-block-components-product-button,
.wp-block-woocommerce-product-template .wp-block-woocommerce-product-button {
	display: none;
}


/* -------------------------------------------------------------------------
   7. Archive
   -------------------------------------------------------------------------
   The two-column shell itself is Bones'. What is here is the surrounding
   rhythm and the toolbar rule, which are Ridgeline's.
   ------------------------------------------------------------------------- */

.ub-archive__head h1 {
	margin-block: var(--wp--preset--spacing--30) 0;
}

.ub-archive__intro {
	color: var(--wp--preset--color--muted);
	max-inline-size: 52ch;
	margin-block-start: var(--wp--preset--spacing--40);
}

.ub-archive__toolbar {
	border-bottom: 1px solid var(--wp--preset--color--line);
	padding-block-end: var(--wp--preset--spacing--40);
}


/* -------------------------------------------------------------------------
   8. Single product
   -------------------------------------------------------------------------
   One column on mobile, two from 782px with the buy column stuck. The gallery
   takes the larger share; the buy column holds a fixed comfortable width.
   ------------------------------------------------------------------------- */

.rl-product {
	display: flex;
	flex-wrap: wrap;
	gap: var(--wp--preset--spacing--90);
}

.rl-product__gallery {
	flex: 999 1 340px;
	min-inline-size: 0;
}

/* Woo pins the gallery to a floated 512px box and nothing in the theme was
   undoing it — which is why the product shot sat small and hard left with a
   third of the page empty beside it. Measured: gallery column 1115px wide,
   gallery inside it 512px, image 512x440, `float: left`. The declaration comes
   from `woocommerce-layout.css` — `.woocommerce div.product div.images
   { width: 48%; float: left }`, 48% of a 1070px column being the 512 — so it
   is Woo's own published styling rather than anything the theme asked for.

   Note the shape of this selector. Woo's is 0,3,2; the obvious three-class
   override is 0,3,0 and silently loses on element count, which is exactly what
   happened on the first attempt — the rule was there, the gallery stayed
   floated at 512. Qualifying with `div` and the `images` class Woo itself
   targets makes it 0,4,1, which wins outright. Still no `!important`. */
.rl-product .rl-product__gallery div.woocommerce-product-gallery.images {
	float: none;
	inline-size: 100%;
	max-inline-size: 100%;
	margin-inline: auto;
}

/* The 512 came from TWO places and only one of them was the float. The block
   wrapper also carries `max-width: 512px`, so releasing the float alone left
   the gallery exactly where it was — worth knowing, because "the rule is in
   the file and nothing moved" reads like a specificity problem when it is
   actually a second constraint one level up.

   The cap is deliberate. Left to fill the column the image renders 1070px
   wide from a 600x515 source — a 1.8x upscale that shows. 820 keeps it large
   and centred without visible softening; the real ceiling is the source, so if
   this should go bigger the fix is WooCommerce > Settings > Products > single
   image width, then regenerate thumbnails. Raising `galleryMax` alone just
   upscales further. */
.rl-product .rl-product__gallery .wp-block-woocommerce-product-image-gallery {
	inline-size: 100%;
	max-inline-size: min(100%, var(--wp--custom--rl--gallery-max, 820px));
	margin-inline: auto;
}

.rl-product .rl-product__gallery div.images .woocommerce-product-gallery__image img,
.rl-product .rl-product__gallery div.images .woocommerce-product-gallery__wrapper img {
	inline-size: 100%;
	block-size: auto;
	object-fit: contain;
}

.rl-product .rl-product__gallery .flex-control-thumbs {
	justify-content: center;
}

/* The thumbnail strip, and the 768px of dead page it was making.
   ------------------------------------------------------------------------
   Woo's classic gallery lays its thumbnails out as a wrapping grid with no
   ceiling. A variable product carrying an image per variation therefore
   renders every one of them: measured on Evil Offering, 24 thumbnails, 768px
   tall, which is what pushed the buy column into a screen of empty space and
   made the page read as broken. That is data, not a bug — the product really
   does have 24 images — but a catalogue page cannot be as tall as its worst
   product.

   One scrolling row instead, thumbnails at a fixed size. `flex-control-thumbs`
   is flexslider's published markup, shipped and documented by Woo, the same
   class of dependency as `wc-block-*`. */
.rl-product__gallery .flex-control-thumbs {
	display: flex;
	gap: var(--wp--custom--upline--gap-sm);
	overflow-x: auto;
	overscroll-behavior-x: contain;
	margin: var(--wp--preset--spacing--40) 0 0;
	padding: 0;
	list-style: none;
	scrollbar-width: thin;
}

.rl-product__gallery .flex-control-thumbs li {
	flex: 0 0 clamp(56px, 18vw, 84px);
	margin: 0;
}

.rl-product__gallery .flex-control-thumbs img {
	inline-size: 100%;
	aspect-ratio: 1 / 1;
	object-fit: cover;
	border: 1px solid var(--wp--custom--upline--rule);
	border-radius: var(--wp--custom--upline--radius);
	cursor: pointer;
	opacity: 0.6;
	transition: opacity var(--wp--custom--upline--motion-duration) var(--wp--custom--upline--motion-easing);
}

.rl-product__gallery .flex-control-thumbs img.flex-active,
.rl-product__gallery .flex-control-thumbs img:hover {
	opacity: 1;
	border-color: var(--wp--preset--color--contrast);
}

/* The buy column's width is a design constant, not a stray hardcoded number.
   Its job is to put its left edge on the same vertical as the header's search
   box, so the page has one right-hand alignment instead of two near-misses.
   Measured at 1728: wide container 124 -> 1604, search box left edge 1238, so
   1604 - 1238 = 366. It lives in `settings.custom.rl.buyCol` because it is
   derived from the header's actions cluster, and if that cluster changes width
   the two must move together — which is exactly the drift a shared token
   exists to prevent.

   Honest limit: the header actions and the product row are independent flex
   clusters, so this aligns at desktop widths and drifts as the actions shrink.
   A single source for both would need the header to publish its own width. */
.rl-product__buy {
	flex: 0 1 var(--wp--custom--rl--buy-col, 366px);
}

@media (min-width: 782px) {
	/* THIS OFFSET WAS 50.9px SHORT AND THE COLUMN SAT BEHIND THE HEADER — fixed
	   2.15.15.
	   --------------------------------------------------------------------
	   It read `--rl--header-h-stuck` (52), a number about the bar's COLLAPSED
	   height, while what is actually above the content is the whole sticky
	   template part — announcement plus bar, 122.9 — which did not shrink
	   because the collapse has never run. Measured on bikeco's tall-gallery
	   product at 1440, where the column has 300.9px of real sticky slack:

	     scrollY 300   buy y 104, header part bottom 154.9   → 50.9 BEHIND
	     scrollY 450   buy y 104, header part bottom 154.9   → 50.9 BEHIND

	   and the first thing in this column is the brand line. Live on every
	   product page at desk, and this file's.

	   IT NOW READS `--ub-collapse-from`, THE BAR'S RESTING HEIGHT, AND THAT IS
	   DELIBERATELY NOT `--to`. The ruled fix was `--to` (44), which is the
	   right quantity the day the collapse actually runs and the wrong one until
	   then — measured, `--to` leaves 29px of overlap today because the bar is
	   still 73. `--from` clears it:

	     --to   (44) + gap   buy top 64   bar bottom 93   → 29 behind
	     --from (72) + gap   buy top 92   bar bottom 73   → 19px of air ✓

	   (19 rather than 20 because `.ub-header` carries a 1px bottom border.)

	   ONE-TOKEN SWAP OWED: when Fault A lands and the bar genuinely collapses,
	   `--ub-collapse-from` becomes `--ub-collapse-to` here and the 19px of air
	   stays 19px instead of becoming a 28px gap. The two changes land together;
	   shipping a known 29px overlap now in order to be correct later is the
	   wrong way round.

	   NO SUM IS CORRECT IN BOTH STATES, which is the real lesson and why this
	   carries a swap rather than a clever expression: a collapsing header's
	   height is not a constant, and a sibling sticky that offsets by a constant
	   is wrong in one state or the other. CSS has no "height of that other
	   sticky element" today. */
	.rl-product__buy {
		position: sticky;
		top: calc(
			var(--ub-sticky-top, 0px)
			+ var(--ub-collapse-from, 72px)
			+ var(--wp--preset--spacing--60)
		);
		align-self: start;
	}
}

/* The add-to-cart form is classic WooCommerce markup, and that is unavoidable.
   ------------------------------------------------------------------------
   `woocommerce/add-to-cart-form` is a block in name only — it renders
   `woocommerce_template_single_add_to_cart()`, i.e. the classic variations
   table, quantity stepper and submit button. There is no block equivalent that
   handles variable products, so the choice is not "blocks or classic", it is
   "styled classic or unstyled classic". These are Woo's own published classes.

   The mockup's swatch UI (SLATE / BONE / MOSS, S–XXL as buttons) is a
   different control, not a restyle of a `<select>`. Faking it from CSS would
   leave the real select behind it doing the work; that belongs in a plugin.
   What this does is make the real control look like it belongs here.

   The label wrapping one word per line — "Size / and / Build / Kit" down the
   left of the page — was the table's `th` collapsing to its minimum width. */
.rl-product__buy table.variations {
	inline-size: 100%;
	border-collapse: collapse;
	margin-block: var(--wp--preset--spacing--50);
}

.rl-product__buy table.variations th,
.rl-product__buy table.variations td {
	display: block;
	inline-size: auto;
	text-align: start;
	padding: 0;
	border: 0;
}

.rl-product__buy table.variations th {
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--mono-sm);
	letter-spacing: var(--wp--custom--rl--mono-track, 0.14em);
	text-transform: uppercase;
	padding-block-start: var(--wp--preset--spacing--50);
	padding-block-end: var(--wp--preset--spacing--20);
}

/* The clipped "Choose an option".
   ------------------------------------------------------------------------
   `woocommerce-blocktheme.css` sets `.woocommerce table.variations tr td
   select { height: 3em; padding: 0.9em 1.1em }`. At 14px that is a 42px box
   with 25.2px of vertical padding, leaving 16.8px of content area for a 30px
   line-height — and the select's `overflow: clip` takes the difference off the
   text. Measured: scrollHeight 43 against clientHeight 42, glyphs cut.

   `height: auto` and a line-height that fits are the fix; `min-block-size`
   keeps the tap floor. Woo's selector is 0,2,3 — this one is 0,3,3, so it wins
   on specificity alone, no `!important`. */
.rl-product .rl-product__buy table.variations td select {
	inline-size: 100%;
	block-size: auto;
	min-block-size: var(--wp--custom--upline--tap);
	line-height: 1.3;
	padding-block: 0.6em;
	padding-inline: 0.9em 2.4em;
}

.rl-product__buy .woocommerce-variation-add-to-cart,
.rl-product__buy form.cart {
	display: flex;
	flex-wrap: wrap;
	align-items: stretch;
	gap: var(--wp--custom--upline--gap-sm);
	margin-block-start: var(--wp--preset--spacing--50);
}

.rl-product__buy .quantity input.qty {
	inline-size: 5rem;
	min-block-size: var(--wp--custom--upline--tap);
	text-align: center;
}

.rl-product__buy .single_add_to_cart_button {
	flex: 1 1 12rem;
	min-block-size: var(--wp--custom--upline--tap);
	border: 0;
	border-radius: var(--wp--custom--upline--radius);
	background-color: var(--wp--custom--upline--accent);
	color: var(--wp--custom--rl--accent-fg, #ffffff);
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--mono-sm);
	letter-spacing: var(--wp--custom--rl--mono-track, 0.14em);
	text-transform: uppercase;
	cursor: pointer;
}

.rl-product__buy .single_add_to_cart_button.disabled {
	opacity: 0.45;
	cursor: not-allowed;
}

.rl-product__brand {
	color: var(--wp--custom--rl--accent-ink);
}

.rl-product__price {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: var(--wp--preset--spacing--40);
}

.rl-product__was {
	color: var(--wp--preset--color--muted);
	text-decoration: line-through;
}

.rl-product__save {
	color: var(--wp--custom--rl--accent-ink);
}

/* The assurances list. Sized for a fourth item, because the P2 in-store
   availability line lands here and must not force a redesign. */
.rl-product__assurances {
	list-style: none;
	margin-block: var(--wp--preset--spacing--70);
	padding-block: var(--wp--preset--spacing--60);
	padding-inline: 0;
	border-block: 1px solid var(--wp--preset--color--line);
	display: grid;
	gap: var(--wp--preset--spacing--40);
	font-size: var(--wp--preset--font-size--x-small);
}

/* The spec table placeholder for the P2 block. Scrolls in its own container
   rather than the page — Build Spec §4.5, using Bones' escape hatch. */
.rl-product__specs {
	inline-size: 100%;
	border-collapse: collapse;
	font-size: var(--wp--preset--font-size--x-small);
}

.rl-product__specs th,
.rl-product__specs td {
	text-align: start;
	padding-block: var(--wp--preset--spacing--40);
	border-bottom: 1px solid var(--wp--preset--color--line);
}

.rl-product__specs th {
	font-family: var(--wp--preset--font-family--mono);
	letter-spacing: var(--wp--custom--rl--mono-track, 0.14em);
	text-transform: uppercase;
	font-weight: 500;
	color: var(--wp--preset--color--muted);
	inline-size: 40%;
}


/* -------------------------------------------------------------------------
   9. Cart and checkout
   -------------------------------------------------------------------------
   Bones already removed Woo's chrome and re-pointed its borders and radii at
   the tokens. What is left for Ridgeline is proportion: the summary card, the
   line-item rhythm, and the accent on the one button that matters.

   These are `wc-block-*` selectors, which is Woo's published surface for
   themes — unlike upline-facets internals, which are private and off limits.
   ------------------------------------------------------------------------- */

.wc-block-cart__sidebar .wc-block-components-sidebar,
.wc-block-checkout__sidebar .wc-block-components-sidebar {
	border: 1px solid var(--wp--preset--color--contrast);
	border-radius: var(--wp--custom--upline--radius);
	padding: var(--wp--preset--spacing--70);
}

.wc-block-cart-items__row {
	border-bottom: 1px solid var(--wp--preset--color--line);
	padding-block: var(--wp--preset--spacing--60);
}

.wc-block-components-product-name {
	font-weight: 700;
	letter-spacing: -0.01em;
}

.wc-block-components-order-summary__button-text,
.wc-block-components-totals-item__label {
	font-size: var(--wp--preset--font-size--x-small);
}

/* The checkout CTA. The one place in cart where accent is a fill. */
.wc-block-cart__submit-button,
.wc-block-components-checkout-place-order-button {
	background-color: var(--wp--preset--color--accent);
	color: var(--wp--custom--rl--accent-fg);
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--mono-sm);
	letter-spacing: var(--wp--custom--rl--mono-track, 0.14em);
	text-transform: uppercase;
	min-block-size: 54px;
	border: 0;
}

.wc-block-cart__submit-button:hover,
.wc-block-components-checkout-place-order-button:hover {
	background-color: var(--wp--preset--color--contrast);
}

/* THE QUANTITY STEPPER — 2.11.11. TWO DECLARATIONS, AND ONLY ONE OF THEM IS
   THE LOOK.

   Reported as "there's a box around the quantity and the delete, it's
   misaligned". Measured on the cart page and again in the mini-cart drawer,
   same numbers on both: the stepper's border box ends at x=327 and its `+`
   button runs 311-341 — FOURTEEN PIXELS PAST ITS OWN BORDER — and the trash
   icon starts at 339, so the two controls overlap by 2.

   THE BORDER WAS NOT CAUSING IT. Removing the border alone was tested first
   and moved the `+` by one pixel: 310-340 against a trash at 339, still
   overlapping. A borderless stepper with two controls on top of each other is
   a tidier version of the same defect, so the second declaration is not
   scope creep — it is the half that fixes what was reported.

   THE ARITHMETIC. Woo ships the input `box-sizing: content-box` with
   `width:40px` and `padding:8px 10px`, so it occupies 60 and cannot shrink
   below it, while the flex box sized itself from an intrinsic 107. 30+60+30
   into 105. One declaration puts the input on the same box model as everything
   else around it and the row fits: `+` lands 297-327, inside its own box, 12
   clear of the trash. Verified in both surfaces — cart page and the mini-cart
   drawer — before this was written.

   WHY IT IS HERE AND NOT IN BONES. The chassis is right to leave the input
   alone in the abstract: `content-box` is Woo's declaration and reversing it in
   the parent changes the geometry of every skin at once, including ones that
   solved it their own way. Ridgeline is the one asking for the borderless
   stepper, so Ridgeline takes the width consequence that comes with it. If a
   second skin arrives with the same two lines, that is the signal to promote
   the box-model half — not before.

   Bones sets `border-color` on this box from the rule token and keeps doing so;
   this only takes the border away for THIS skin, which is exactly the seam. */
.wc-block-components-quantity-selector {
	border: 0;
}

.wc-block-components-quantity-selector__input {
	box-sizing: border-box;
}


/* -------------------------------------------------------------------------
   10. Hero
   ------------------------------------------------------------------------- */

.rl-hero {
	position: relative;
	display: flex;
	align-items: flex-end;
	min-block-size: clamp(460px, 74vh, 720px);
	background-color: var(--wp--preset--color--inverse);
	color: var(--wp--preset--color--inverse-text);
	background-size: cover;
	background-position: 50% 30%;
}

.rl-hero::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(
		to top,
		color-mix(in srgb, var(--wp--preset--color--inverse) 78%, transparent),
		transparent 62%
	);
	pointer-events: none;
}

/* Hard left, not centred. `.rl-hero` is a flex container and this box is
   capped at a 46ch measure, so without an explicit start margin the `alignwide`
   auto margins centre it in the flex line — measured at 717px wide with 490px
   of dead space either side, against a mockup that sets it flush to the same
   left gutter as every section heading below it. This is the constrained-layout
   centring scar again, arriving through flex rather than through the layout
   engine. */
.rl-hero__copy {
	position: relative;
	z-index: 1;
	max-inline-size: 46ch;
	margin-inline: 0 auto;
	padding-block: var(--wp--preset--spacing--100);
}

.rl-hero__copy h1 {
	margin-block: var(--wp--preset--spacing--40);
}


/* -------------------------------------------------------------------------
   11. Split featured panels
   -------------------------------------------------------------------------
   Two panels side by side from 782px, stacked below. The accent panel is the
   one place in the design where accent is a FILL rather than a highlight, so
   its ink comes from the accentFg token instead of inheriting — the same value
   the buttons use, so the two can never drift apart.
   ------------------------------------------------------------------------- */

.rl-split {
	display: grid;
	grid-template-columns: 1fr;
	gap: var(--wp--custom--upline--gap, 10px);
}

@media (min-width: 782px) {
	.rl-split {
		grid-template-columns: repeat(2, minmax(300px, 1fr));
	}
}

.rl-panel {
	display: flex;
	flex-direction: column;
	justify-content: flex-end;
	gap: var(--wp--preset--spacing--40);
	min-block-size: clamp(260px, 34vw, 380px);
	padding: var(--wp--preset--spacing--90);
	border-radius: var(--wp--custom--upline--radius);
}

.rl-panel--dark {
	background-color: var(--wp--preset--color--inverse);
	color: var(--wp--preset--color--inverse-text);
}

.rl-panel--accent {
	background-color: var(--wp--preset--color--accent);
	color: var(--wp--custom--rl--accent-fg);
}

.rl-panel a {
	color: inherit;
}

/* The accent panel is a fill, so neither ink value belongs on it: the light ink
   on the accent ground measures 1.4:1. Its links keep the panel's own contrast
   colour through hover — the same white the fill was pinned to. */
.rl-panel--accent a:hover {
	color: var(--wp--custom--rl--accent-fg);
}

.rl-panel h3 {
	margin: 0;
}


/* -------------------------------------------------------------------------
   12. Services — the Booked landing zone
   -------------------------------------------------------------------------
   Build Spec §5: reserve room STRUCTURALLY so an empty zone collapses and a
   filled one inherits the section's rhythm. A min-height would leave a hole on
   a shop with no services; a flex column with a consistent gap leaves nothing
   at all, and spaces the service menu correctly the moment Booked has data.
   ------------------------------------------------------------------------- */

.rl-services__menu {
	display: flex;
	flex-direction: column;
	gap: var(--wp--custom--upline--gap, 10px);
}

/* -------------------------------------------------------------------------
   Contact — skinning Bones' slots
   -------------------------------------------------------------------------
   The structure is Bones' `patterns/contact.php`: named, empty containers for
   address, hours, phone, map and form. This file only paints them, which is the
   whole point of the split — a drastically different Upline theme skins the same
   five slots without rebuilding the layout.

   Every rule here targets a SLOT CLASS and, at most, one bare element inside it.
   Nothing walks the markup, because the markup inside a slot is expected to
   change: today the hours are a typed list, tomorrow they are a Base-fed block
   with its own structure, and a rule shaped like `.ub-contact__hours > div > p`
   would pass gate 6 and break silently on swap day.

   Slot headings take the mono signature rather than the heading scale. They are
   labels for a value, not section headings competing with the page title —
   the same treatment the variation-picker axis labels and the spec table's
   `th` already carry, so three label systems on one site read as one. */

.ub-contact__intro p {
	max-inline-size: 46ch;
}

.ub-contact__address h2,
.ub-contact__phone h2,
.ub-contact__hours h2,
.ub-contact__email h2 {
	font-family: var(--wp--preset--font-family--mono);
	font-size: 11px;
	font-weight: 500;
	letter-spacing: 0.14em;
	text-transform: uppercase;
	color: var(--wp--preset--color--muted);
	margin-block-end: var(--wp--preset--spacing--30);
}

.ub-contact__email a {
	font-size: var(--wp--preset--font-size--large);
	text-decoration: none;
	border-block-end: 1px solid var(--wp--custom--rl--accent-ink);
}

/* The map slot is empty until a shop pastes an embed, and an empty slot
   collapses to nothing on purpose — a reserved grey box would be exactly the
   "designed empty state" the build spec bans in favour of honesty. What this
   does is make whatever lands there behave: an embed is responsive and keeps
   the editorial ratio instead of arriving at its own hardcoded pixel size. */
.ub-contact__map iframe {
	inline-size: 100%;
	aspect-ratio: var(--wp--custom--rl--editorial-ratio, 3 / 2);
	border: 1px solid var(--wp--custom--upline--rule);
	display: block;
}

/* The form sits below the location set and reads as a separate offer, so it
   gets the section rule the front page uses between bands. */
.ub-contact__form {
	border-block-start: 1px solid var(--wp--custom--upline--rule);
	padding-block-start: var(--wp--preset--spacing--70);
}

/* -------------------------------------------------------------------------
   6d — THE SALE PAGE AND THE SALE PROMO
   -------------------------------------------------------------------------
   Read on bikeco at `/sale/fall-sale/`, facets 1.46.3, before a line of this
   was written. The markup, in full, for the `live` state:

       <div class="upline-sale wp-block-upline-facets-sale-page"
            data-upline-sale-state="live">
         <h1 class="upline-sale__title">Fall Sale</h1>
       </div>

   Three facts that shape every rule below. The state lands on the WRAPPER, not
   on a child, so it is available as a parent hook. `__title` is an `h1` and a
   DIRECT child. And in the `live` state there is no `__note` element at all —
   not an empty one, none — which settles the §8 question the right way: the
   plugin omits rather than emits-and-collapses, so this file needs no `:empty`
   rule and no collapse guard.

   WHAT IS STYLED AND WHAT IS DELIBERATELY NOT. The contract declares four
   states — live, scheduled, over, unavailable — and only `live` has been
   RENDERED anywhere this bench can reach. The wrapper and title rules below
   apply to all four, because they key on elements every state must have to be a
   page at all. The state-specific treatments do not exist yet, and writing them
   would be styling classes never seen rendered, which this bench has declined
   twice on the same grounds. When a scheduled or ended sale is observable, the
   note's treatment is a small addition to this section, not a rewrite.

   No `panelBearing` obligation here: the contract declares
   `upline-facets/sale-page` and `upline-facets/sale-promo` both `panelBearing:
   false`, READ from the served 1.45.0/1.46.x map rather than taken from the
   flag. The plugin paints no ground on either, so the theme's own ink applies
   and no `styles.blocks` carve-out is owed.
   ------------------------------------------------------------------------- */

/* The sale title carries the page, so it takes the archive title's role rather
   than inventing a third heading scale. Bones hides `.wp-block-query-title` on
   this route (2.3.19), so this is the page's only h1 and the rhythm below it has
   to be the one the head group would have provided. */
.upline-sale {
	margin-block-end: var(--wp--preset--spacing--80);
}

.upline-sale__title {
	font-size: var(--wp--preset--font-size--xx-large);
	line-height: 1.05;
	margin: 0;
}

/* -------------------------------------------------------------------------
   The sale promo — designated, so now styled
   -------------------------------------------------------------------------
   `.upline-sale-promo` and `.upline-sale-promo__link` are in the Public
   stylable surfaces table of `contract/theme-tokens.md`, read out of
   `upline-facets-1.48.1.zip` (50365 B, sha256 62e8534b95eabc5b…). Gate 6
   rejected this treatment while they were undesignated and accepts it now for
   the only reason that should ever have changed it: the owning bench published
   the names.

   THREE FACTS THE DOC STATES, and each one changes a declaration here.

   (1) THE CONTAINER HOLDS A LIST, NOT A LINK. It wraps one `__link` per LIVE
   sale page and a shop may run several at once, with no separator element
   shipped. So the container is styled as a set — a wrapping flex row with a
   gap — and the link as a member of it. The doc warns that a rule written
   against a single child is correct on a shop with one sale and wrong on the
   first shop with two; `gap` is what makes two links legible without the
   plugin having to emit a divider this theme would then have to style.

   (2) IT IS ABSENT RATHER THAN EMPTY. No markup at all when no sale is live,
   so there is NO empty state and nothing to reserve — hence no `min-height`,
   no placeholder, and no `:empty` rule. Measured both ways on bikeco before
   this was written: present on `/product-category/bikes/`, and no markup at all
   on the sale page itself. The block also sits in normal flow in
   `archive-product.html` rather than as a flex or grid child, so an absent
   promo costs exactly zero space.

   (3) THE PLUGIN SHIPS NO PAINT for either name. These rules start from
   nothing rather than overriding, so no layer or specificity question arises —
   unlike `.upline-availability*` below, which the plugin does paint.

   It reads as an offer rather than navigation, so it takes the mono signature
   the chips and the spec labels already use; a fourth label system would be
   three too many. The rule above it separates it from the archive title
   without adding a box, so it never competes with the products.
   ------------------------------------------------------------------------- */

.upline-sale-promo {
	display: flex;
	flex-wrap: wrap;
	gap: var(--wp--preset--spacing--30) var(--wp--preset--spacing--50);
	margin-block-end: var(--wp--preset--spacing--70);
	padding-block-start: var(--wp--preset--spacing--40);
	border-block-start: 1px solid var(--wp--custom--upline--rule);
}

.upline-sale-promo__link {
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--mono-sm);
	text-transform: uppercase;
	letter-spacing: 0.06em;
	display: inline-flex;
	align-items: center;
	min-height: var(--wp--custom--upline--tap);
}

/* -------------------------------------------------------------------------
   The PDP collect panel — the one designated surface the plugin paints
   -------------------------------------------------------------------------
   Six designated names: `.upline-availability` and its `__lead`, `__list`,
   `__line`, `__dot` and `__note`. Read on bikeco after 1.47.6 landed:
   `data-upline-availability="890"`, inside `<main>`, immediately after the
   add-to-cart form, and the filled sentence reads "Only 2 available at Second
   Shop / Collect today at Second Shop · Just arrived".

   ONLY THE CONTAINER CARRIES `hidden`, so only the container's rule is scoped
   to `:not([hidden])`. The doc puts the attribute on `.upline-availability` and
   nowhere else, and adding a defensive `:not([hidden])` to `__list` or `__line`
   would be guarding a state this bench has not been told exists — the same
   reason the non-live sale states are still unstyled. Scope where the attribute
   is documented to be.

   THE ATTRIBUTE DOES THE HIDING, AND THIS FILE MUST NOT TAKE THAT OVER. The
   container is server-rendered EMPTY with `hidden`, and the browser fetches the
   branch data and fills it — verified by fetching the PDP's server HTML, where
   the element is present and its text is the empty string. So every rule that
   sets a `display` is scoped `:not([hidden])`. Without that scope a bare
   `display` on the class would beat the UA stylesheet's `[hidden] { display:
   none }` and pin an empty panel onto every product page until the fetch
   returned — and permanently, on a product Base has nothing to say about,
   which the doc names as the failure. Style the filled state; let the
   attribute hide the unfilled one.

   AND IT IS ABSENT, NOT EMPTY, when there is nothing to fetch at all: an
   unconnected shop or a product with no SKU renders no markup. So there is
   still nothing to reserve space for, and no `min-height` here either.

   WHY THESE ARE PLAIN SELECTORS even though the plugin paints them. The doc
   advises "only a later layer" rather than a deeper selector, which is right in
   general and resolves differently here: this stylesheet is ALREADY unlayered,
   unlayered CSS beats layered CSS, and gate 6 forbids `@layer` in theme CSS for
   exactly that reason. So a plain class selector already wins, and writing the
   layer the doc mentions would fail this theme's own gate.

   WHAT IS DELIBERATELY NOT STYLED: `__dot`'s colour. The dot is a STATUS mark
   and the plugin owns what each status means; recolouring it here would be this
   theme deciding a semantics it cannot see the full set of. Layout and
   typography are the skin's; status colour is not.
   ------------------------------------------------------------------------- */

.upline-availability:not([hidden]) {
	display: block;
	margin-block-start: var(--wp--preset--spacing--50);
	padding-block-start: var(--wp--preset--spacing--40);
	border-block-start: 1px solid var(--wp--custom--upline--rule);
}

.upline-availability__lead {
	font-size: var(--wp--preset--font-size--small);
	margin: 0;
}

.upline-availability__list {
	display: flex;
	flex-direction: column;
	gap: var(--wp--custom--upline--gap-sm, 6px);
	list-style: none;
	margin: var(--wp--preset--spacing--30) 0 0;
	padding: 0;
}

.upline-availability__line {
	display: flex;
	align-items: baseline;
	gap: var(--wp--preset--spacing--30);
	font-size: var(--wp--preset--font-size--small);
}

.upline-availability__note {
	font-family: var(--wp--preset--font-family--mono);
	font-size: var(--wp--preset--font-size--mono-sm);
	color: var(--wp--preset--color--muted);
	margin-block-start: var(--wp--preset--spacing--30);
}
/* ===========================================================================
   2c. The menu, in Ridgeline's voice — 2.15.8
   ---------------------------------------------------------------------------
   Ben's standing rule: headers and menus carry the theme. Whatever option a
   shop picks, the drawer, its panes, the back control, the scrim, the sidebar
   rail and the bottom bar read as this skin's, not as the plugin's defaults.
   Behaviour stays the plugin's.

   Against plugin 1.54.1, which designates 40 surfaces (32 -> 40 at 1.54.0,
   the first movement in that table since 1.50.0) and publishes the three type
   registers plus the slide pair as tokens.
   ---------------------------------------------------------------------------
   Every value below is taken from Ridgeline's existing system rather than
   invented: the mono-caps register (§9's "signature is mono 11px uppercase at
   0.14em"), the accent inks split by ground (§10), the one-panel-pair rule
   (§6), the kept heading underlines and footer rule (§18.2, §22), the removed
   desktop edge (§22), and the icon family at stroke 1.25 (§15).

   THE TYPE ARRIVES AS TOKENS, NOT AS CSS. `theme.json` sets the bar, label
   and leaf registers; nothing below restates them. The values and their
   sources are in the release record — the short version is that the bar and
   the label both take this theme's mono-caps signature
   (`--wp--preset--font-size--mono-sm` 11px, weight 500,
   `--wp--custom--rl--mono-track` 0.14em, uppercase), and the leaf takes the
   body face, because a shop's product names are its words and not our label.
   ======================================================================== */

/* ---------------------------------------------------------------------------
   1 · The scrim — the skin's veil, not a plugin grey
   ---------------------------------------------------------------------------
   Built from `inverse`, which is the ground the announce strip and the footer
   already use, so the dimmed page reads as the same dark the design uses
   elsewhere rather than as a neutral wash. 78% keeps the page legible behind
   it — the drawer is a layer over the shop, not a new page.
   The plugin paints no scrim colour of its own; this is paint where there was
   none, not an override. */
.upline-menu__scrim {
	background-color: color-mix(in srgb, var(--wp--preset--color--inverse) 78%, transparent);
}

/* ---------------------------------------------------------------------------
   2 · The toggle and close pair — one control, two states
   ---------------------------------------------------------------------------
   Bones already sizes both to the tap floor and masks both with Ridgeline's
   icon slots at stroke 1.25. What the skin adds is that they read as a PAIR:
   the same ink. Before this they were a masked glyph on nothing.

   2.15.11: THE HAIRLINE AND THE CORNER CAME OFF. Ben: "the pin lines should not
   be there around every element." The wireframes draw a box around each control
   and those boxes are PLACEMENT MARKS, not borders — this skin read them as a
   spec and boxed the toggle, the close and the search toggle in a 1px rule with
   a 2px corner. Measured on bikeco before the cut: all three computed
   `1px solid rgba(15,15,15,.18)`, radius 2px.

   The tap box is untouched. `min-block-size: var(--upline--tap)` is Bones'
   accessibility floor and has nothing to do with the border; removing the rule
   leaves three 44x44 controls that are still 44x44 and simply have no outline.
   Measured after: all three `0px none`, radius 0, boxes unchanged. */
.upline-menu__toggle,
.upline-menu__close {
	color: currentColor;
}

/* THE CLOSE CONTROL'S INVERSE-SIDE HAIRLINE WENT WITH THE BORDER — 2.15.11.
   It read `border-color: color-mix(… inverse-text 22% …)`, because the close
   sits on the drawer's dark ground and needed the opposite-side rule. With the
   border gone there is nothing left for it to colour: a `border-color` with no
   `border-style` paints nothing, so keeping it would have been a declaration
   that looks like a decision and does nothing — the kind of furniture §24
   retired.

   The §10 note it carried is worth keeping even though its rule is not: a
   value that is right on the light ground is not automatically right on the
   dark one, which applies to this skin's ink everywhere and is why the close
   needed its own line in the first place.

   It also carried gate 6's correction, which still stands and still binds
   anything written here next: this was once `.upline-menu__drawer
   .upline-menu__close`, and neither `__drawer` nor `--drawer` is a designated
   surface. The scope was never needed — the renderer emits the close control
   only inside the drawer — and a scope that names an undesignated class to
   restate what the markup already guarantees is reach without purpose. */

/* ---------------------------------------------------------------------------
   3 · The back control — new at 1.54.0, and it is a LABEL
   ---------------------------------------------------------------------------
   The sliding panes need a way back, and the plugin now designates the
   control, its icon and its label separately. The label takes the mono-caps
   register; the icon takes the family. Treated as a quiet affordance rather
   than a button: it is a retreat, and a retreat should not compete with the
   category the shopper is looking at.

   The glyph is Bones' `iconBack` slot, added in the paired 2.3.31 and
   overridden here at stroke 1.25 with the rest of the family — gate 16 is
   all-or-none, so the sixth slot arrives in both artifacts or neither.

   2.15.9: THIS SECTION SHIPPED THE RULE THAT BROKE EVERY DESK DRAWER SHOP, and
   it was this bench's, not the plugin's. Worth the space, because the shape is
   general and will be tempting again.

   As shipped at 2.15.8 the first rule opened `display: flex; align-items:
   center` and carried NO media query. The plugin hides the closed control with
   `display: none` inside `@layer upline`, and an unlayered theme declaration
   beats a layered one at any specificity — the asymmetry this bench measured at
   1.53.1 and wrote down. So the skin's `display` un-hid a control the plugin
   had hidden, the control landed in the desk drawer's flow, and the header grew
   to 397px on bikeco. The plugin bench reproduced it: bare range showed a
   closed drawer at 0px with back `display: none`; adding this one rule gave
   264px and rows `44px 220px`.

   TWO THINGS WERE WRONG, and the plugin's 1.54.3 fix only makes one of them
   unreachable:
     `display` IS THE MECHANISM, NOT THE DRESSING. Section 16's line is that a
       theme styles the control and never replaces the mechanism, and presence
       is the most mechanical property there is. The plugin's own step-3 rule
       already sets `display: flex` and `align-items: center`, so these two
       declarations bought nothing and cost a release.
     NO BOUNDARY CITATION. Every other rule in the 2.15.8 dressing took the
       plugin's number; this one took none, so it applied below 761 where this
       control does not exist at all — the back control and its icon are
       declared only inside `@media (min-width: 761px)`, re-enumerated against
       1.54.3 with media context, which is the reading 2.3.31 and its first
       correction both skipped.

   1.54.3 now removes the control from the DOCUMENT rather than hiding it, so
   this rule can no longer resurrect it. That is the plugin defending against
   this skin, not this skin being right, which is why the fix ships anyway.

   MEASURED BEFORE IT WAS PROPOSED: at 1440, step 3, full stack, pixels compared
   over one window fixed from the shipped case's rect — as shipped, with the two
   declarations dropped, and dropped-and-scoped are ALL PIXEL-IDENTICAL, and
   computed `display` stays `flex` because the plugin supplies it. Both
   mutations were then parsed by the engine to confirm they were valid CSS still
   carrying the whole dressing, because a mutation guard proves the text changed
   and not that it changed into CSS.

   The whole section moves inside the query, not just the first rule: a
   dressing rule whose subject cannot appear below the boundary is dead code
   there, and the hover and label rules are the same control. 44px itself stays
   — that floor is the chassis's `:where()` tap rule on every button, an
   accessibility rule with nothing to do with this menu, and with the closed
   control out of flow it has nothing left to inflate. */
@media (min-width: 761px) {
	.upline-menu__back {
		gap: var(--wp--preset--spacing--20);
		min-block-size: var(--wp--custom--upline--tap);
		padding-inline: var(--wp--preset--spacing--30);
		border-block-end: 1px solid color-mix(in srgb, var(--wp--preset--color--inverse-text) 14%, transparent);
		color: color-mix(in srgb, var(--wp--preset--color--inverse-text) 72%, transparent);
		text-decoration: none;
	}

	.upline-menu__back:hover,
	.upline-menu__back:focus-visible {
		color: var(--wp--preset--color--inverse-text);
	}

	.upline-menu__back-label {
		font-family: var(--wp--preset--font-family--mono);
		font-size: var(--wp--preset--font-size--mono-xs);
		font-weight: 500;
		letter-spacing: var(--wp--custom--rl--mono-track, 0.14em);
		text-transform: uppercase;
	}
}

/* ---------------------------------------------------------------------------
   4 · The bar — designated at 1.54.0
   ---------------------------------------------------------------------------
   On the desk this is the row of top-level categories; in the drawer it is
   the first pane. One rule for both, because §6's ruling is that the menu has
   one identity: the divider between entries is the same hairline the archive
   rail and the tiles use, read from the ground it sits on.

   `> * + *` REACHES THE ENTRIES WITHOUT NAMING THEM, and that is the second
   correction gate 6 forced: `.upline-menu__item` is not designated even now
   that `.upline-menu__bar` is. The universal child is the legitimate idiom for
   "whatever the plugin puts in its own container" — the same shape gate 25
   recognises for a constrained block's child — and it survives the plugin
   renaming the item class, which the tokens doc warns it may. */
.upline-menu__bar > * + * {
	border-block-start: 1px solid color-mix(in srgb, currentColor 14%, transparent);
}

@media (min-width: 761px) {
	/* On the desk the bar is horizontal, so a top border between items would
	   draw across the row. The plugin's breakpoint, cited from the boundary
	   table at 1.54.0 — not WordPress's 782. */
	.upline-menu__bar > * + * {
		border-block-start: 0;
	}
}

/* ---------------------------------------------------------------------------
   5 · The panel's interior — headings, lists, columns, footer
   ---------------------------------------------------------------------------
   Ben ruled the six heading underlines and the footer rule KEPT (§18.2,
   confirmed §22), and the desktop outer edge REMOVED (§22, shipped 2.11.9).
   So the panel's structure is settled design and this only gives it the
   skin's hairline rather than the plugin's white one.

   The headings' type comes from the label tokens, not from here. */
.upline-mega__heading {
	border-block-end-color: color-mix(in srgb, currentColor 18%, transparent);
}

.upline-mega__footer {
	border-block-start-color: color-mix(in srgb, currentColor 18%, transparent);
}

/* The pane separators Ben's drawings explicitly keep — same treatment, so the
   panel reads as one ruled system rather than three borrowed ones. */
.upline-mega__col + .upline-mega__col {
	border-inline-start-color: color-mix(in srgb, currentColor 18%, transparent);
}

/* A shop's own words, in the shop's own voice: the leaf list is Archivo at
   the body register, which is what the leaf tokens now ask for. The only
   thing CSS adds is the hover, because there is no token for a state. */
.upline-mega__list a:hover,
.upline-mega__list a:focus-visible {
	color: var(--wp--custom--rl--accent-on-inverse);
}

.upline-mega__aside {
	font-family: var(--wp--preset--font-family--sans);
	color: color-mix(in srgb, var(--wp--preset--color--inverse-text) 72%, transparent);
}


/* ===========================================================================
   7 · The spec charts — 2.15.22
   ---------------------------------------------------------------------------
   `.upline-specs__table` is designated at plugin 1.59.2 and the doc's own
   phrasing is "everything about how it looks is yours". Everything here is
   inside that designation. `.upline-specs__scroll` — the overflow box, its
   `tabindex` and its focus stop — is INTERNAL and nothing below touches it.

   THE READING SURFACE IS NOT ONE SHAPE, AND ONE RULE ACROSS BOTH MAKES THE
   SMALLER ONE WORSE. Measured on bikeco, 2026-10-19:

     pairs    2 columns, outside the scroll region, `white-space: normal`
              El Jefe 12 rows, Flyway 53, Top Shelf 1. Wraps; never scrolls.
     matrix   N columns inside the scroll region at `nowrap`, sticky label
              column. El Jefe Build Kits 22x4, geometry 16x5; Flyway Size
              19x7; Top Shelf Size chart 5x18.

   A first pass capped every cell at 10ch and made the PAIRS table TALLER at
   390 — 609px to 653px — because a two-column table does not want its value
   column narrowed; it wants the line to run. The cap is therefore scoped to
   matrices by `:has(tr > *:nth-child(4))`, a structural test a two-column
   table cannot match at any row. Split that way the pairs table gets SHORTER
   instead: 609 -> 423.

   WHAT ACTUALLY FIXES THE WIDTH IS UNSETTING THE PLUGIN'S `nowrap`. Its
   fallback is in `@layer upline`, so one unlayered declaration here beats it
   with no `!important` — the inversion working in the direction it was built
   for. Measured, content width against the 992px box at 1024 and the 358px
   box at 390:

     Build Kits 22c   x5.49 -> x1.74   |  x15.21 -> x4.83
     geometry 16c     x2.49 -> x1.13   |  x6.91  -> x3.14
     Flyway Size 19c  x2.70 -> x1.37   |  x7.49  -> x3.79
     Flyway Frame 2c  x1.02 -> x1.00   |  x2.84  -> x1.00

   The last row is the one that was broken rather than tight: a two-column
   name/value table that the plugin had put inside the scroll region at
   `nowrap`, so at 1024 a reader saw the label column and NOT the value. It
   now fits at every width with no horizontal scroll at all.

   WHAT IS NOT CLAIMED HERE. Build Kits still scrolls, and no amount of type
   fixes that: 22 columns in 992px is 45px a column. It is wide because it is
   TRANSPOSED — 21 components across and 3 kits down, where a buyer reads a
   component and compares kits. The plugin owns the orientation and is cutting
   a render rule for it; at 21 rows x 4 columns this table stops scrolling
   entirely and these rules keep working unchanged.

   The edge affordance is also not here. At rest there is no cue that columns
   exist off-screen — the scroll box reserves no scrollbar space
   (`offsetHeight - clientHeight` is 0; macOS overlay bars are invisible until
   touched). Drawing it needs to know WHICH edge, because the label column is
   sticky and a fade at the start edge at scroll 0 paints against that column
   and reads as a fault. Plugin 1.59.3 publishes `data-upline-scroll` =
   none|start|mid|end for exactly that, and the treatment lands when the
   attribute is designated. Progressive by construction: nothing below depends
   on it.
   ======================================================================== */

.upline-specs__table {
	font-size: var(--wp--preset--font-size--x-small);
	line-height: 1.4;
	/* Figures in a spec chart are read in columns and compared down them, so
	   they get the same advance width. This is the one thing in the brief that
	   was simply missing: the tables computed `font-variant-numeric: normal`. */
	font-variant-numeric: tabular-nums;
}

.upline-specs__table th,
.upline-specs__table td {
	white-space: normal;
	padding: 7px 10px;
}

/* MATRICES ONLY — four or more cells in a row. The pairs table cannot match
   this selector, which is the whole point of writing it structurally rather
   than by section name: the section names are the brand's and they move. */
.upline-specs__table:has(tr > *:nth-child(4)) td {
	max-width: 10ch;
}

/* The label column stays on one line so the sticky column reads as a column
   rather than a ragged edge. `th[scope=row]` already computes an opaque
   background and sticks — that is the plugin's and it works; nothing here
   re-implements it. */
.upline-specs__table:has(tr > *:nth-child(4)) th[scope='row'] {
	white-space: nowrap;
	max-width: 22ch;
}


/* ===========================================================================
   7a · The PDP description's own list — 2.15.22
   ---------------------------------------------------------------------------
   Base's descriptions arrive as limited HTML from a closed alphabet of seven
   bare tags, lists among them, and a bare `ul` takes the UA's 40px indent
   while the plugin's features list sits at `1.2em`. Two lists on one page at
   two indents.

   THE VALUE IS `1.2em` AND NOT `18px`, AND THE DIFFERENCE IS THE WHOLE POINT.
   18px is what `1.2em` RESOLVES TO against this skin's 15px body — it is a
   derived number, not a designed one, and writing the literal forks the two
   lists apart the first time a body size moves. Bones' own `medium` is 16px,
   where the plugin's list goes to 19.2 and a literal 18 sits still. Same
   unit, locked by construction.

   Nothing is written for `.upline-specs__features`: the plugin's value is
   already the one the design wants, so the theme brings its own list to meet
   it rather than restating it. One rule, one owner for the number.

   SCOPED TO THE DESCRIPTION PANEL, not to the details block. The only `ul`
   inside `wp-block-woocommerce-product-details` on El Jefe is Woo's own TAB
   LIST, and a rule reaching that would indent the tabs. Woo's
   `woocommerce-Tabs-panel--description` is a published classic class (§6), so
   it is fair to name; depth 1 only, because the sanitiser flattens any list
   opened inside a list and a depth-2 rule would be guarding a state that
   cannot arrive. */
.woocommerce-Tabs-panel--description > ul,
.woocommerce-Tabs-panel--description > ol {
	padding-inline-start: 1.2em;
}


/* ===========================================================================
   7b · The archive head's band — 2.15.22
   ---------------------------------------------------------------------------
   Ben's ruling: 44 at desk. The band measured 108 and the arithmetic everyone
   agreed on was 88 of section padding plus 20 of block-gap — right about the
   total and wrong about the owner, which cost a round.

   `main` is `display: block` with `row-gap: normal`. THERE IS NO BLOCK-GAP.
   WordPress emits a flow layout's `blockGap` as `margin-block-start` on the
   SIBLING, so the 20px belongs to `.ub-archive__head` itself, taken because
   it follows the store-notices div as a `* + *`. The first fix set
   `display: none` on the empty notices block and the band came out 64, not
   44: `display: none` does not stop a selector matching, so the head kept its
   margin. Measured, not reasoned — the rule below names the real owner.

   `:empty` does NOT match the notices block: it holds an empty
   `.woocommerce-notices-wrapper` child, so the selector that reads naturally
   is the one that fails. Checked live, all four candidates.

   Measured with both rules in place: 108 -> 44 at 1024, 72 -> 52 at 390. The
   44 is desk-only, so an iPad tightens by 64px in landscape and 20px in
   portrait — one device, two amounts, both as ruled.

   44 is written as a literal and not as `spacing--90`, which also happens to
   be 44px: that step is the tap scale, this band is not a tap target, and
   coupling them would move the band the day a fingertip minimum moves. */
@media (min-width: 782px) {
	.rl-section:has(> .ub-archive__head) {
		padding-block-start: 44px;
	}
}

.wp-block-woocommerce-store-notices:has(> .woocommerce-notices-wrapper:empty)
	+ .ub-archive__head {
	margin-block-start: 0;
}


/* ==== UB SHELL MODES — START (gate 23 boundary) ============================
   Ridgeline's shell rules live in one contiguous span so the claim that they
   are ADDITIVE is provable rather than asserted: delete everything between
   these two markers and the skin renders the plain page, exactly as a shop
   that has chosen no shell already does. Same contract as the chassis's own
   region, which is where this delimiter idiom comes from.

   Every selector here is anchored at its start on `body.upline-shell--*` —
   the three body classes the plugin publishes and the contract names — so
   nothing in this block can reach a page that has not asked for a shell. */
/* ---------------------------------------------------------------------------
   6 · The three shells
   ---------------------------------------------------------------------------
   Bones sizes them; the skin gives them a surface and a seam. All three read
   as the same system: the header's own ground, divided from the page by the
   same hairline the header already uses.
   ------------------------------------------------------------------------ */
body.upline-shell--sidebar .wp-site-blocks > header {
	background-color: var(--wp--preset--color--base);
	border-inline-end: 1px solid var(--wp--custom--upline--rule);
}

body.upline-shell--bottom-bar .ub-shell-bar {
	background-color: var(--wp--preset--color--base);
}

body.upline-shell--bottom-bar .ub-shell-bar--bottom {
	border-block-start: 1px solid var(--wp--custom--upline--rule);
}

body.upline-shell--bottom-bar .ub-shell-bar--top {
	border-block-end: 1px solid var(--wp--custom--upline--rule);
}

/* The pushed page keeps a seam so the drawer does not appear to melt into the
   content it has moved aside. */
body.upline-shell--push:has(.upline-menu__switch:checked) .wp-site-blocks {
	border-inline-start: 1px solid var(--wp--custom--upline--rule);
}

/* ==== UB SHELL MODES — END (gate 23 boundary) ============================ */

/* ---------------------------------------------------------------------------
   7 · The search toggle's glyph, beside the menu's
   ---------------------------------------------------------------------------
   They sit adjacent in every option, so they share the treatment above.
   Bones masks the glyph; the skin only has to not fight it.

   2.15.11: ITS HAIRLINE CAME OFF with the toggle's and the close's — Ben's
   ruling that the wireframes' boxes are placement marks, not borders.

   THE `border: 0` EARLIER IN THIS FILE IS NOT REDUNDANT NOW THAT THIS RULE IS
   GONE, AND IT WILL LOOK IT. This control is a `<button>`, unlike the menu
   toggle and the close, which are `<label>`s — so Bones'
   `:where(button:not(.wp-element-button):not(.wp-block-button__link))` DOES
   match it and does set `border: 1px solid var(--upline--rule)`. The only thing
   keeping the chassis hairline off this control is the skin's own `border: 0`
   up in the search section. Delete that as a tidy-up and the border returns,
   from the parent, with nothing in this section to explain where it came from.

   Honest limit on that warning: it is REASONED, not measured. Testing it needs
   the declaration deleted from the file and re-rendered — adding `border:
   initial` in a probe is still a declaration that wins and proves nothing,
   which is what the first attempt did. The cut itself IS measured: with both
   re-adds gone, the toggle, the close and this control all compute `0px none`
   at radius 0, boxes unchanged at 44x44. */
