/*
Theme Name: Neve FSE Child
Theme URI: https://github.com/the-comet-companies/neve-fse-child
Description: Shop Titan child theme for Neve FSE. Holds the converted block patterns (formerly ACF Flexible-Content blocks) rebuilt as native FSE patterns that consume the parent theme's theme.json design tokens (color / typography / spacing presets).
Author: The Comet Companies
Author URI: https://thecometcompany.com
Template: neve-fse
Version: 1.44.29
Requires at least: 6.5
Tested up to: 6.7
Requires PHP: 7.4
Text Domain: neve-fse-child
*/

/*
 * Styling lives in theme.json / Global Styles, not here.
 * Add only genuinely-custom CSS that presets cannot express.
 */

/* ── A DATED MEASUREMENT BELONGS IN THE TRAIL, NOT IN A COMMENT (2026-09-30) ──
 *
 * From today a machine keeps the history this file used to keep by hand. Every
 * night, for every store, page and width, the trail records the key elements'
 * rectangles and the winning stylesheet and selector for each probed property,
 * and keeps 90 nights of them; every deploy, and every un-deployed change to a
 * store's database styles, is one append-only ledger line, forever. One command
 * reads it all back:
 *
 *     python3 tools/floor_trail.py why <store> <path> --selector "<sel>"
 *     (shoptitan-core; it prints the first night a rectangle or a cascade
 *      winner moved, what landed between that night and the one before, the
 *      pull requests inside it, and whether any of them claimed that element)
 *
 * SO STOP WRITING `Measured on 2026-09-21: the rail was 240x60` HERE. The trail
 * holds that per night; a comment holds one frozen copy that nobody updates and
 * that is wrong the next time the page moves.
 *
 * WHAT STAYS, AND IT IS ALMOST ALL OF IT. The floor guardrails said 51% of the
 * theme's layer is comments "because history is kept in comments". Re-measured
 * on `main`, 2026-09-30: the percentage is right about THIS FILE and wrong
 * about the layer — style.css is 53.9% comments
 * (106,196 of 196,948 bytes), while all 203 css/scss files together are
 * 19.8% (164,590 of 830,625). And the reason does not hold at all:
 * 22 dated sentences totalling 2,945 bytes are 2.8% of this file's comments
 * (`tools/css_comment_ratio.py style.css --dated` lists them), and every one of
 * them is the EVIDENCE for a decision in the same comment — why a selector
 * carries `body#dtla_body_id.dtla__body`, why `:where()` is load-bearing, why
 * not one hex appears in the filter rail, which of Diego's rules this file
 * overrides and how to undo it. Cutting them leaves an assertion with no proof,
 * which is worse than the duplication, so NOTHING was moved out. The trail
 * records that a rectangle moved. It cannot record why a rule is shaped the way
 * it is, and nothing here should be cut on the theory that it can.
 *
 * RE-MEASURE, NEVER RE-QUOTE. Those three numbers are a measurement too, and
 * this comment is subject to its own rule: `python3 tools/css_comment_ratio.py`
 * prints them for every css and scss file in the theme. If you are about to
 * repeat a percentage from here in a pull request, run it first.
 *
 * (And no, a regex for a CSS comment cannot be written in one: its own text
 *  carries `*` next to `/`, which ends the block at that character. It did,
 *  here, for about four minutes.)
 */

/* ── Base · Body-copy lists hang their markers ──────────────────────────────
   A `wp:list` in flow copy inherits the browser's 40px `padding-inline-start`,
   so its text sits 40px right of the heading and paragraph above it while
   every other section on the page is flush. Design review 2026-08-12 called
   this out on /team-apparel; it is not a page bug, the same list ships on 11
   pages (every decoration-method page, services, corporate-merch) and on the
   six footer link columns.

   The fix is a hanging marker: the list TEXT lines up with the headings and
   the bullet sits out in the gutter. That is what was asked for — the marker
   stays, it just stops pushing the text inward.

   `:where()` is load-bearing. It zeroes the rule's specificity, so this reads
   as "flush is the default for a list that has no opinion of its own" and any
   list that DOES carry its own padding keeps it: `.st-assurances` at (0,1,0)
   still wins with its 1.1rem, and it would have lost to a bare
   `ul.wp-block-list` at (0,2,0). Author origin still beats the UA sheet
   regardless of specificity, which is why zero is enough to clear the 40px.

   Markers hang into the section gutter, which is
   `clamp(24px, 3.333vw, 48px)` from the parent theme, so there is room for
   them at every width. Measured after: text at 340px against a 340px heading
   at 1440 wide, 24px against 24px at 390 wide, no horizontal overflow either
   time. Nested lists keep an indent of their own.                          */
:where(ul.wp-block-list, ol.wp-block-list) {
	padding-inline-start: 0;
	list-style-position: outside;
}
:where(.wp-block-list .wp-block-list) {
	padding-inline-start: 1.5em;
}

/* ── LEGACY · Custom-HTML logo marquee ──────────────────────────────────────
   st-trusted-by used to ship its marquee as a Custom HTML block with the logo
   set pasted in twice. It is now ordinary blocks driven by assets/marquee.js
   (see "Block marquee" below), but pages that inserted the old version still
   hold that raw HTML, so these rules stay until those pages are re-inserted.
   Do not use .st-logo-marquee in new work.                                   */
.st-logo-marquee {
	width: 100%;
	overflow: hidden;
	-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
	        mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.st-logo-marquee__track {
	display: flex;
	flex-wrap: nowrap;
	width: max-content;
	align-items: center;
	gap: 3.5rem;
	animation: st-marquee 32s linear infinite;
}
.st-logo-marquee:hover .st-logo-marquee__track {
	animation-play-state: paused;
}
.st-logo-marquee__item {
	flex: 0 0 auto;
	margin: 0;
	white-space: nowrap;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	opacity: 0.7;
}
@keyframes st-marquee {
	from { transform: translateX(0); }
	to   { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
	.st-logo-marquee__track {
		animation: none;
		flex-wrap: wrap;
		justify-content: center;
		width: 100%;
	}
}

/* ── Shop Titan · Receipt spec card (pattern: st-receipt) ───────────────────
   Frame + dashed itemized rules that give the "printed receipt" look. Colours
   come from theme.json presets (ti-fg / ti-line); only the frame lives here.  */
.st-receipt {
	max-width: 460px;
	margin-inline: auto;
	border: 1px solid var(--wp--preset--color--ti-fg);
}
.st-receipt-hd {
	border-bottom: 1px dashed var(--wp--preset--color--ti-line);
	padding-bottom: 0.85rem;
	margin-bottom: 0.35rem;
}
.st-receipt-row {
	gap: 1rem;
	padding-top: 0.55rem;
	padding-bottom: 0.55rem;
	border-bottom: 1px dashed var(--wp--preset--color--ti-line);
}
.st-receipt-row:last-child {
	border-bottom: 0;
}
.st-receipt-row p {
	margin: 0;
}

/* ── Shop Titan · Store facts strip (pattern: st-store-facts) ───────────────
   Rows of "what to expect", each one a single [st_fact] sentence. Colours come
   from theme.json presets; only the hairline and the empty-row collapse live
   here.

   THE :empty RULE IS LOAD-BEARING, not polish. A store that has not answered a
   fact renders an EMPTY <p> — the shortcode drops the prefix and the suffix with
   the value, which is the whole safety model in inc/store-facts.php. Without
   this, that empty paragraph still draws its rule and its gap, so an unanswered
   line reads as a blank row somebody forgot to fill in. `display: none` makes
   the sentence genuinely absent, which is what "publishes nothing" has to mean
   on a page as well as in the markup.                                        */
.st-store-facts .st-store-fact {
	margin: 0;
	padding-top: 0.85rem;
	border-top: 1px solid var(--wp--preset--color--ti-line);
}
.st-store-facts .st-store-fact:first-child {
	border-top: 0;
	padding-top: 0;
}
.st-store-facts .st-store-fact:empty {
	display: none;
}

/* ── Shop Titan · Accordion (patterns: st-process-steps, st-faq) ────────────
   Hairline-ruled core/details list. Colour comes from theme.json presets on
   the blocks; only the rules, the marker swap and the step counter live here.
   Add the class `st-accordion` to the group wrapping the details blocks —
   add `st-steps` as well to get automatic 01/02/03 numbering.               */
.st-accordion > .wp-block-details {
	border-top: 1px solid var(--wp--preset--color--ti-line);
	padding: 1.15rem 0;
	margin: 0;
}
.st-accordion > .wp-block-details:last-child {
	border-bottom: 1px solid var(--wp--preset--color--ti-line);
}
.st-accordion > .wp-block-details > summary {
	display: flex;
	align-items: baseline;
	gap: 0.9rem;
	cursor: pointer;
	list-style: none;
	font-weight: 700;
	line-height: 1.35;
}
.st-accordion > .wp-block-details > summary::-webkit-details-marker {
	display: none;
}
.st-accordion > .wp-block-details > summary::after {
	content: "+";
	margin-left: auto;
	flex: 0 0 auto;
	font-family: var(--wp--preset--font-family--space-mono);
	font-weight: 400;
	color: var(--wp--preset--color--ti-accent-secondary);
}
.st-accordion > .wp-block-details[open] > summary::after {
	content: "\2013"; /* en dash */
}
.st-accordion > .wp-block-details > summary:focus-visible {
	outline: 2px solid var(--wp--preset--color--ti-fg);
	outline-offset: 3px;
}
.st-accordion > .wp-block-details > *:not(summary) {
	margin-top: 0.85rem;
	/* Keep a reading measure, but not so tight that answers look left-clipped
	   against a full-width question. 62ch filled barely half the 860px column
	   and read as a layout bug; 90ch is the upper bound of a comfortable measure
	   and sits at roughly three-quarters. It is a token — `custom.stMeasureWide`
	   — beside the body measure, so the two move together. Row WB-254. */
	max-width: var(--wp--custom--st-measure-wide, 90ch);
}

/* Auto-numbering — renumbers itself when steps are added, moved or removed. */
.st-steps {
	counter-reset: st-step;
}
.st-steps > .wp-block-details {
	counter-increment: st-step;
}
.st-steps > .wp-block-details > summary::before {
	content: counter(st-step, decimal-leading-zero);
	flex: 0 0 auto;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 400;
	letter-spacing: 0.06em;
	color: var(--wp--preset--color--ti-accent-secondary);
}
.st-steps > .wp-block-details > *:not(summary) {
	padding-left: 3.1rem;
}

/* ── Shop Titan · Why-us differentiators (pattern: st-why-us) ───────────────
   Numbered, rule-topped points. Counter numbering matches the accordion so a
   reordered point renumbers itself.                                         */
.st-why-list {
	counter-reset: st-why;
}
.st-why-item {
	counter-increment: st-why;
	position: relative;
	padding-top: 1.5rem;
	padding-left: 3.1rem;
	border-top: 1px solid var(--wp--preset--color--ti-line);
}
.st-why-item::before {
	content: counter(st-why, decimal-leading-zero);
	position: absolute;
	top: 1.5rem;
	left: 0;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: var(--wp--preset--font-size--small);
	letter-spacing: 0.06em;
	line-height: 1.6;
	color: var(--wp--preset--color--ti-accent-secondary);
}
@media (max-width: 781px) {
	.st-why-item {
		padding-left: 0;
	}
	.st-why-item::before {
		position: static;
		display: block;
		margin-bottom: 0.35rem;
	}
}

/* ── Shop Titan · How-it-works step cards (pattern: st-how-it-works) ────────
   Framed cards with an oversized outlined "ghost" numeral, counter-driven so
   adding or reordering a card renumbers the set.                            */
.st-hiw-grid {
	counter-reset: st-hiw;
}
.st-hiw-card {
	counter-increment: st-hiw;
	position: relative;
	overflow: hidden;
	padding: 2.25rem 1.75rem 1.75rem;
	border: 1px solid var(--wp--preset--color--ti-line);
}
.st-hiw-card::before {
	content: counter(st-hiw, decimal-leading-zero);
	position: absolute;
	top: -0.3em;
	right: 0.08em;
	font-size: 7rem;
	font-weight: 800;
	line-height: 1;
	letter-spacing: -0.04em;
	color: transparent;
	-webkit-text-stroke: 1px var(--wp--preset--color--ti-line);
	opacity: 0.4;
	pointer-events: none;
}
.st-hiw-card > * {
	position: relative;
}
.st-hiw-detail {
	margin-top: 1.25rem;
	padding-top: 0.9rem;
	border-top: 1px dashed var(--wp--preset--color--ti-line);
	letter-spacing: 0.04em;
}

/* Scroll-triggered rise, CSS-only. Gated on @supports so unsupporting
   browsers simply show the cards, and skipped inside the editor canvas
   (body.editor-styles-wrapper) where the scroll container is the iframe. */
@supports (animation-timeline: view()) {
	@media (prefers-reduced-motion: no-preference) {
		body:not(.editor-styles-wrapper) .st-hiw-card {
			animation: st-hiw-rise linear both;
			animation-timeline: view();
			animation-range: entry 8% cover 26%;
		}
	}
}
@keyframes st-hiw-rise {
	from {
		opacity: 0;
		transform: translateY(14px);
	}
	to {
		opacity: 1;
		transform: none;
	}
}

/* ── Shop Titan · Full-height column panels ─────────────────────────────────
   The columns stretch to the tallest panel, but a cover or group nested
   inside one does not inherit that height — it sits at its own content
   height and leaves a gap under the shorter side. Make each column a flex
   column so its panel fills it, and both sides end level whatever the copy
   length. Used by st-dual-cta and st-feature-media-full; add st-fill-columns
   to any Columns block that needs the same. (.st-dual-cta is kept as its own
   selector because pages inserted before st-fill-columns existed carry it.) */
.st-dual-cta > .wp-block-column,
.st-fill-columns > .wp-block-column {
	display: flex;
	flex-direction: column;
}
.st-dual-cta > .wp-block-column > .wp-block-cover,
.st-fill-columns > .wp-block-column > .wp-block-cover,
.st-fill-columns > .wp-block-column > .wp-block-group {
	flex: 1 0 auto;
}

/* ── Shop Titan · Logo wall (pattern: st-logo-wall) ─────────────────────────
   Grid cells hold logos at a common height; mono treatment keeps a wall of
   mismatched client files looking like one set.                             */
.st-logo-wall .wp-block-image {
	margin: 0;
	display: flex;
	align-items: center;
	justify-content: center;
}
.st-logo-wall .wp-block-image img {
	max-height: 3rem;
	width: auto;
	object-fit: contain;
	filter: grayscale(1);
	opacity: 0.6;
	transition: opacity 0.2s ease, filter 0.2s ease;
}
.st-logo-wall .wp-block-image img:hover {
	filter: grayscale(0);
	opacity: 1;
}

/* ── Shop Titan · Production standards (pattern: st-production-standards) ───
   Faint press-registration rules behind the section, and spec cards with a
   heavy top accent. Card framing lives here so the block attributes stay
   editable without fighting an inline style block.                          */
.st-prod-std {
	position: relative;
	overflow: hidden;
}
.st-prod-std::before {
	content: "";
	position: absolute;
	inset: 0;
	pointer-events: none;
	background: repeating-linear-gradient(
		90deg,
		transparent,
		transparent calc(25% - 0.5px),
		rgba(0, 0, 0, 0.05) calc(25% - 0.5px),
		rgba(0, 0, 0, 0.05) calc(25% + 0.5px)
	);
}
.st-prod-std > * {
	position: relative;
}
.st-prod-card {
	padding: 1.75rem 1.5rem;
	background: var(--wp--preset--color--ti-bg-alt);
	border: 1px solid var(--wp--preset--color--ti-line);
	border-top: 3px solid var(--wp--preset--color--ti-fg);
}
.st-prod-method {
	margin-bottom: 0.85rem;
}
.st-prod-label {
	margin-top: 1.25rem;
	padding-top: 0.9rem;
	border-top: 1px dashed var(--wp--preset--color--ti-line);
	margin-bottom: 0.35rem;
}

/* ── Shop Titan · Durability proof (pattern: st-durability-proof) ───────────
   Method chips, then staggered test cards with an oversized counter number
   set in the left margin. Stagger is decorative and drops away on mobile.  */
/* .st-method-chip is styled with .st-chip further down. */
.st-dur-tests {
	counter-reset: st-dur;
}
.st-dur-test {
	counter-increment: st-dur;
	position: relative;
	padding-left: 5.5rem;
	padding-top: 1.75rem;
	border-top: 1px solid var(--wp--preset--color--ti-accent-secondary);
	max-width: 46rem;
}
.st-dur-test::before {
	content: counter(st-dur, decimal-leading-zero);
	position: absolute;
	left: 0;
	top: 1.25rem;
	font-size: 3rem;
	font-weight: 800;
	line-height: 1;
	letter-spacing: -0.04em;
	color: transparent;
	-webkit-text-stroke: 1px var(--wp--preset--color--ti-accent-secondary);
}
@media (min-width: 782px) {
	.st-dur-test:nth-child(2) {
		margin-left: 6rem;
	}
	.st-dur-test:nth-child(3) {
		margin-left: 12rem;
	}
}
@media (max-width: 781px) {
	.st-dur-test {
		padding-left: 0;
		padding-top: 3.75rem;
	}
	.st-dur-test::before {
		top: 1.25rem;
	}
}

/* ── Shop Titan · Block marquee (patterns: st-trust-strip, st-trusted-by) ───
   The items are ordinary blocks. assets/marquee.js clones them until the
   track is two identical halves each at least as wide as the frame, then
   sets --st-marquee-duration from --st-marquee-speed (px per second), so a
   short strip and a long one travel at the same rate. Until that runs — no
   JS, or reduced motion — the track is just a wrapped, centred flex row,
   which is also how it appears in the editor.                              */
.st-marquee__viewport {
	width: 100%;
	overflow: hidden;
}
.st-marquee__track.is-animated {
	flex-wrap: nowrap;
	justify-content: flex-start;
	width: max-content;
	animation: st-marquee-scroll var(--st-marquee-duration, 30s) linear infinite;
}
.st-marquee:hover .st-marquee__track.is-animated {
	animation-play-state: paused;
}
.st-marquee__item {
	flex: 0 0 auto;
	margin: 0;
	white-space: nowrap;
}
@keyframes st-marquee-scroll {
	from { transform: translateX(0); }
	to   { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
	.st-marquee__track {
		animation: none;
	}
	.st-marquee__viewport {
		-webkit-mask-image: none;
		        mask-image: none;
	}
}

/* Trust strip — hairline-divided claims on a dark band. */
.st-trust-strip .st-marquee__viewport {
	-webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
	        mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}
.st-trust-strip .st-marquee__track {
	--st-marquee-speed: 90;
	row-gap: 0.5rem;
}
.st-trust-strip .st-marquee__item {
	padding: 0 1.75rem;
	letter-spacing: 0.04em;
	border-left: 1px solid var(--wp--preset--color--ti-accent-secondary);
}
/* A store that has not written its claims renders no track at all (see
   inc/trust-claims.php), which would otherwise leave an empty dark band across
   the page. Collapse the band with it. A browser without :has() shows a thin
   strip rather than a broken one, so this stays a progressive enhancement. */
.st-trust-strip:not(:has(.st-marquee__track)) {
	display: none;
}

/* Trusted-by — spaced uppercase wordmarks, no dividers. */
.st-logo-strip .st-marquee__viewport {
	-webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
	        mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.st-logo-strip .st-marquee__track {
	--st-marquee-speed: 70;
	row-gap: 1rem;
}
.st-logo-strip .st-marquee__item {
	padding: 0 1.75rem;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	opacity: 0.7;
}
.st-logo-strip .st-marquee__item img {
	max-height: 2.5rem;
	width: auto;
}

/* ── Shop Titan · Use-case cards (pattern: st-use-cases) ────────────────────
   Framed text cards that sit alongside the image panel in the same grid.   */
.st-use-card {
	display: flex;
	flex-direction: column;
	padding: 1.5rem;
	background: var(--wp--preset--color--ti-bg-alt);
	border: 1px solid var(--wp--preset--color--ti-line);
}
.st-use-cta {
	margin-top: auto;
	padding-top: 1rem;
	letter-spacing: 0.04em;
}
.st-use-cta a {
	text-decoration: none;
}
.st-use-cta a:hover {
	text-decoration: underline;
}

/* ── Shop Titan · Use-case rail (pattern: st-use-cases-slider) ──────────────
   Every use case is the same card with its own photograph, laid on a
   scroll-snapping rail instead of one big panel plus three small text boxes.
   Pure CSS until assets/carousel.js adds arrows: with no JS it is still a
   swipeable, keyboard-scrollable row, and in the editor every card stays
   directly editable rather than being buried behind a control.
   --st-rail-cols is how many cards a full-width rail shows.                 */
.st-rail {
	--st-rail-cols: 3;
	--st-rail-gap: 1.25rem;
	position: relative;
}
.st-rail__track {
	display: flex;
	gap: var(--st-rail-gap);
	overflow-x: auto;
	scroll-snap-type: x mandatory;
	scroll-behavior: smooth;
	scrollbar-width: none;
	/* Room for the card border and focus ring, which a clipped rail would
	   otherwise shave off the first and last card. */
	padding: 2px 2px 1rem;
	margin: -2px -2px 0;
}
.st-rail__track::-webkit-scrollbar {
	display: none;
}
.st-rail__card {
	flex: 0 0
		calc(
			(100% - (var(--st-rail-cols) - 1) * var(--st-rail-gap)) /
				var(--st-rail-cols)
		);
	scroll-snap-align: start;
	/* The stretched heading link resolves against this, not against .st-rail —
	   without it one card's link would cover the whole rail. */
	position: relative;
	display: flex;
	flex-direction: column;
	overflow: hidden;
	background: var(--wp--preset--color--ti-bg-alt);
	border: 1px solid var(--wp--preset--color--ti-line);
	transition: border-color 0.2s ease;
}
.st-rail__card:hover {
	border-color: var(--wp--preset--color--ti-fg);
}
.st-rail__card > .wp-block-image {
	margin: 0;
	overflow: hidden;
}
.st-rail__card > .wp-block-image img {
	display: block;
	width: 100%;
	height: auto;
	object-fit: cover;
	transition: transform 0.6s cubic-bezier(0.2, 0.6, 0.2, 1);
}
.st-rail__card:hover > .wp-block-image img {
	transform: scale(1.05);
}
.st-rail__body {
	display: flex;
	flex-direction: column;
	flex: 1 1 auto;
	padding: 1.5rem;
}
.st-rail__card .st-use-cta {
	position: relative;
	z-index: 2;
}
/* Same stretched-heading-link contract as the decoration cards. */
.st-rail__card h3 a {
	text-decoration: none;
	color: inherit;
}
.st-rail__card h3 a::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 1;
}
.st-rail__card:hover h3 a {
	text-decoration: underline;
	text-underline-offset: 3px;
}
.st-rail__card h3 a:focus-visible::after {
	outline: 2px solid var(--wp--preset--color--ti-fg);
	outline-offset: -3px;
}
/* Arrows are generated by assets/carousel.js, so they only appear when the
   rail actually overflows — a three-card rail on a wide screen shows none. */
.st-rail__nav {
	display: flex;
	justify-content: flex-end;
	gap: 0.5rem;
	margin-top: 0.75rem;
}
.st-rail__arrow {
	cursor: pointer;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.5rem;
	height: 2.5rem;
	padding: 0;
	background: transparent;
	color: var(--wp--preset--color--ti-fg);
	border: 1px solid var(--wp--preset--color--ti-fg);
	border-radius: 2px;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: 1.25rem;
	line-height: 1;
	transition: background-color 0.2s ease, color 0.2s ease, opacity 0.2s ease;
}
.st-rail__arrow:hover {
	background: var(--wp--preset--color--ti-fg);
	color: var(--wp--preset--color--ti-fg-alt);
}
.st-rail__arrow[disabled] {
	opacity: 0.3;
	cursor: default;
}
.st-rail__arrow[disabled]:hover {
	background: transparent;
	color: var(--wp--preset--color--ti-fg);
}
.st-rail__arrow:focus-visible {
	outline: 2px solid var(--wp--preset--color--ti-fg);
	outline-offset: 3px;
}
@media (max-width: 1023px) {
	.st-rail {
		--st-rail-cols: 2;
	}
}
@media (max-width: 599px) {
	.st-rail {
		/* A sliver of the next card is the only affordance that reads as
		   "this scrolls" without an arrow to point at. */
		--st-rail-cols: 1.15;
	}
}
@media (prefers-reduced-motion: reduce) {
	.st-rail__track {
		scroll-behavior: auto;
	}
	.st-rail__card:hover > .wp-block-image img {
		transform: none;
	}
}

/* ── Shop Titan · Manifesto with images (pattern: st-manifesto-media) ───────
   Each promise links to the page that backs it up, so the section is a way
   into the site rather than a dead band of copy. Image and heading are one
   link; the section's own CTA sits below the three columns.                 */
.st-manifesto-media__col {
	position: relative;
}
.st-manifesto-media__col h3 a {
	text-decoration: none;
	color: inherit;
}
.st-manifesto-media__col h3 a::after {
	content: "";
	position: absolute;
	inset: 0;
}
.st-manifesto-media__col:hover h3 a {
	text-decoration: underline;
	text-underline-offset: 3px;
}
.st-manifesto-media__col h3 a:focus-visible::after {
	outline: 2px solid var(--wp--preset--color--ti-fg);
	outline-offset: 4px;
}
.st-manifesto-media__col:hover .st-manifesto-media__img img {
	transform: scale(1.05);
}
@media (prefers-reduced-motion: reduce) {
	.st-manifesto-media__col:hover .st-manifesto-media__img img {
		transform: none;
	}
}

/* ── Shop Titan · SEO content sections (pattern: st-seo-content) ────────────
   Hairline-separated long-form sections and a rule-framed pull quote.      */
.st-seo-card {
	padding-top: 3rem;
	padding-bottom: 3rem;
	border-bottom: 1px solid var(--wp--preset--color--ti-line);
}
.st-seo-card:first-child {
	padding-top: 0;
}
.st-seo-quote {
	max-width: 46rem;
	margin: 3rem auto 0;
	padding: 0 0 0 1.75rem;
	border-left: 3px solid var(--wp--preset--color--ti-fg);
}
.st-seo-quote cite {
	display: block;
	margin-top: 0.85rem;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: var(--wp--preset--font-size--small);
	font-style: normal;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--wp--preset--color--ti-accent-secondary);
}

/* Marquee strip — bold statement band, phrases divided by a middot. */
.st-marquee-strip .st-marquee__track {
	--st-marquee-speed: 80;
	row-gap: 0.5rem;
}
.st-marquee-strip .st-marquee__item {
	padding: 0 1.5rem;
	font-weight: 800;
	letter-spacing: -0.01em;
}
.st-marquee-strip .st-marquee__item::after {
	content: "\00B7"; /* · */
	margin-left: 3rem;
	opacity: 0.5;
}

/* ── Shop Titan · Media band panel (pattern: st-feature-media-full) ─────────
   The text panel centres its content inside whatever height the image half
   sets, so a short heading does not float at the top of a tall band.       */
.st-media-panel {
	display: flex;
	flex-direction: column;
	justify-content: center;
}

/* ── Shop Titan · Material lab (pattern: st-material-lab) ───────────────────
   Collapsed-border spec table: the wrapper draws the top and left rules,
   each card draws its own right and bottom, so the grid reads as one table
   however many columns it reflows to.                                      */
.st-mat-grid {
	border-top: 1px solid var(--wp--preset--color--ti-line);
	border-left: 1px solid var(--wp--preset--color--ti-line);
}
.st-mat-card {
	padding: 1.5rem;
	border-right: 1px solid var(--wp--preset--color--ti-line);
	border-bottom: 1px solid var(--wp--preset--color--ti-line);
}
.st-mat-spec {
	margin-bottom: 0.75rem;
	padding-bottom: 0.75rem;
	border-bottom: 1px dashed var(--wp--preset--color--ti-line);
	letter-spacing: 0.04em;
}
.st-aside-card {
	padding: 1.75rem 1.5rem;
}
.st-aside-note {
	margin-top: 1.25rem;
	padding-top: 0.9rem;
	border-top: 1px dashed var(--wp--preset--color--ti-accent-secondary);
	letter-spacing: 0.04em;
}

/* ── Shop Titan · Attribute chips (pattern: st-brand-story) ─────────────────
   Shares its look with the durability-proof method chips.                  */
.st-chip,
.st-method-chip {
	margin: 0;
	padding: 0.3rem 0.7rem;
	border: 1px solid var(--wp--preset--color--ti-line);
	letter-spacing: 0.04em;
}
.st-method-chip {
	border-color: var(--wp--preset--color--ti-accent-secondary);
}

/* ── Shop Titan · Decoration zones (pattern: st-decoration-zones) ───────────
   Ruled reference rows, plus a framed guidance note beneath them.          */
.st-zone {
	padding: 1.25rem 0;
	border-bottom: 1px solid var(--wp--preset--color--ti-line);
}
.st-zone:first-child {
	padding-top: 0;
}
.st-zone-spec {
	margin-top: 0.25rem;
	margin-bottom: 0.5rem;
	letter-spacing: 0.04em;
}
.st-zone-note {
	padding: 1.25rem 1.5rem;
	border-left: 3px solid var(--wp--preset--color--ti-fg);
	background: var(--wp--preset--color--ti-bg);
}

/* Collection story — attribute chips on a slow ticker. The chips already
   carry .st-chip, so only spacing and pace are set here. */
.st-attr-strip .st-marquee__track {
	--st-marquee-speed: 45;
	row-gap: 0.6rem;
}
.st-attr-strip .st-marquee__item {
	margin: 0 0.35rem;
}

/* ── Shop Titan · Decoration methods (pattern: st-decoration-methods) ───────
   Framed link cards. The CTA is pinned to the foot so cards with a one-line
   tagline and cards with three lines still align along the bottom.         */
.st-method-card {
	position: relative;
	display: flex;
	flex-direction: column;
	overflow: hidden;
	padding: 1.5rem;
	background: var(--wp--preset--color--ti-bg-alt);
	border: 1px solid var(--wp--preset--color--ti-line);
	transition: border-color 0.2s ease;
}
.st-method-card:hover {
	border-color: var(--wp--preset--color--ti-fg);
}

/* The whole card is the link. The heading anchor carries a stretched ::after
   so image, copy and whitespace all navigate, while the thing announced to a
   screen reader is still "Screen printing, link" rather than an unnamed box.
   The "Explore →" anchor is lifted above that overlay so it keeps its own
   hover state; both point at the same page, which is fine and is why the
   heading link is the one that stretches. */
.st-method-card h3 a {
	text-decoration: none;
	color: inherit;
}
.st-method-card h3 a::after {
	content: "";
	position: absolute;
	inset: 0;
	z-index: 1;
}
.st-method-card:hover h3 a {
	text-decoration: underline;
	text-underline-offset: 3px;
}
.st-method-card h3 a:focus-visible::after {
	outline: 2px solid var(--wp--preset--color--ti-fg);
	outline-offset: -3px;
}
.st-method-card .st-method-link {
	position: relative;
	z-index: 2;
}
.st-method-card.is-featured {
	border-top: 3px solid var(--wp--preset--color--ti-fg);
}
.st-method-link {
	margin-top: auto;
	padding-top: 1rem;
	letter-spacing: 0.04em;
}
.st-method-link a {
	text-decoration: none;
}
.st-method-link a:hover {
	text-decoration: underline;
}

/* ── Shop Titan · Print longevity (pattern: st-print-longevity) ─────────────
   A stamped guarantee seal drawn in CSS, so the wording inside stays real
   editable text rather than baked into an image.                           */
.st-seal {
	display: flex;
	flex-direction: column;
	justify-content: center;
	width: 15rem;
	height: 15rem;
	margin-inline: auto;
	padding: 1.5rem;
	border: 2px solid var(--wp--preset--color--ti-fg-alt);
	border-radius: 50%;
	box-shadow: 0 0 0 6px var(--wp--preset--color--ti-bg-inv), 0 0 0 7px var(--wp--preset--color--ti-accent-secondary);
}
.st-seal > * {
	margin: 0;
}
.st-seal__figure {
	line-height: 1;
	margin-block: 0.35rem;
}
.st-proof {
	padding-top: 1.1rem;
	border-top: 1px solid var(--wp--preset--color--ti-accent-secondary);
}
.st-proof__label {
	margin-bottom: 0.5rem;
	letter-spacing: 0.12em;
}

/* ── Shop Titan · Block carousel (pattern: st-hero-editorial) ───────────────
   Slides are ordinary blocks. assets/carousel.js adds .is-enhanced plus the
   arrows and dots once there are two or more; until then — no JS, one slide,
   or the editor canvas — they stay stacked full-bleed sections with nothing
   hidden. Enhanced, the slides share one grid cell so the carousel is as tall
   as its tallest slide without any absolute positioning.                    */
.st-carousel.is-enhanced {
	position: relative;
	display: grid;
}
.st-carousel.is-enhanced > .st-carousel__slide {
	grid-area: 1 / 1;
	opacity: 0;
	visibility: hidden;
	/* Delay the visibility flip until the fade finishes when hiding, so the
	   outgoing slide can still be seen on its way out — and drop out of the
	   tab order the moment it is gone. */
	transition: opacity 0.6s ease, visibility 0s linear 0.6s;
}
.st-carousel.is-enhanced > .st-carousel__slide.is-active {
	opacity: 1;
	visibility: visible;
	transition: opacity 0.6s ease, visibility 0s;
}
.st-carousel__nav {
	position: absolute;
	z-index: 2;
	left: 0;
	right: 0;
	bottom: 2rem;
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 1.25rem;
}
.st-carousel__arrow,
.st-carousel__dot {
	cursor: pointer;
	background: transparent;
	color: var(--wp--preset--color--ti-fg-alt);
	border: 1px solid var(--wp--preset--color--ti-fg-alt);
	transition: background-color 0.2s ease, opacity 0.2s ease;
}
.st-carousel__arrow {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.5rem;
	height: 2.5rem;
	padding: 0;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: 1.25rem;
	line-height: 1;
}
.st-carousel__arrow:hover,
.st-carousel__dot:hover {
	background: var(--wp--preset--color--ti-fg-alt);
	color: var(--wp--preset--color--ti-fg);
}
.st-carousel__dots {
	display: flex;
	align-items: center;
	gap: 0.5rem;
}
.st-carousel__dot {
	width: 0.75rem;
	height: 0.75rem;
	padding: 0;
	opacity: 0.6;
}
.st-carousel__dot.is-active {
	background: var(--wp--preset--color--ti-fg-alt);
	opacity: 1;
}
.st-carousel__arrow:focus-visible,
.st-carousel__dot:focus-visible {
	outline: 2px solid var(--wp--preset--color--ti-fg-alt);
	outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
	.st-carousel.is-enhanced > .st-carousel__slide {
		transition: none;
	}
}

/* Editorial hero — auto-advance in seconds. Set to 0 for manual only.
   Auto-advance is skipped anyway under prefers-reduced-motion, and pauses
   while the pointer or keyboard focus is inside the hero. */
.st-hero-carousel {
	--st-carousel-autoplay: 7;
}
.st-hero-carousel em {
	font-style: italic;
	color: var(--wp--preset--color--ti-line);
}

/* ── Shop Titan · Sticky showcase (pattern: st-sticky-showcase) ─────────────
   Each panel pins at the top and the next scrolls over it. Pure CSS — no
   scroll listener. The panels must stay opaque or they will show through
   each other, which is why every one carries an overlay.                   */
.st-sticky-panel {
	position: sticky;
	top: 0;
}
/* The last panel should release rather than pin under the following section. */
.st-sticky-showcase > .st-sticky-panel:last-child {
	position: relative;
}
@media (max-width: 781px) {
	.st-sticky-panel {
		position: relative;
	}
}

/* ── Shop Titan · Generic framed card ───────────────────────────────────────
   The plain version of the card treatment used across the patterns, for
   sections that need the frame without a pinned CTA or a ghost numeral.    */
.st-card {
	display: flex;
	flex-direction: column;
	height: 100%;
	padding: 1.5rem;
	background: var(--wp--preset--color--ti-bg-alt);
	border: 1px solid var(--wp--preset--color--ti-line);
}

/* ── Shop Titan · Branch connector (pattern: st-custom-merch) ───────────────
   Drawn on a Spacer block with gradients rather than three inline SVGs: one
   stem down from the headline, a bar across, and a descender into each of
   the three columns. Decorative — Spacer is already aria-hidden. Hidden once
   the columns stack, where it would point at nothing.                      */
@media (min-width: 782px) {
	.st-branch {
		background-image:
			linear-gradient(var(--wp--preset--color--ti-line), var(--wp--preset--color--ti-line)),
			linear-gradient(var(--wp--preset--color--ti-line), var(--wp--preset--color--ti-line)),
			linear-gradient(var(--wp--preset--color--ti-line), var(--wp--preset--color--ti-line)),
			linear-gradient(var(--wp--preset--color--ti-line), var(--wp--preset--color--ti-line)),
			linear-gradient(var(--wp--preset--color--ti-line), var(--wp--preset--color--ti-line));
		background-repeat: no-repeat;
		background-size:
			66.67% 1px,  /* bar across */
			1px 50%,     /* stem down from the headline */
			1px 50%,     /* descender, left column */
			1px 50%,     /* descender, middle column */
			1px 50%;     /* descender, right column */
		background-position:
			center 50%,
			center top,
			16.67% bottom,
			center bottom,
			83.33% bottom;
	}
}

/* ── Shop Titan · Category cards (pattern: st-hero-section) ─────────────────
   The label link is stretched over the whole card so the entire tile is the
   target, not just the few words of text.                                  */
/* A centred flex row rather than a grid: with `1fr` grid columns a row of
   five always stretches edge to edge, and a wrapped orphan sits hard left.
   Flex basis + justify-content keeps the row — and any wrapped remainder —
   centred under the hero copy at every width. */
/* Grid, not flex. As a wrapping flex row the last card on a short row grows to
   fill it — 11 categories at five up left "Bags" stretched across the full
   width, which read as a layout bug. auto-fill keeps every tile the same size
   and lets the orphan sit at one column. */
.st-cat-cards {
	display: grid !important;
	grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
	gap: 1rem;
	max-width: 1360px;
	margin-inline: auto;
}
.st-cat-card {
	position: relative;
	flex: none;
	overflow: hidden;
}

/* Nothing washes over the photo at rest. Cover ships a flat 60% black dim
   (`has-background-dim-60` plus an inline background-color) which turned every
   tile grey; both are overridden here rather than in the page content, so a
   store cannot lose the fix by re-saving the page.
   No scrim either — these tiles are product shots on white, and a gradient
   dark enough to hold white text greys the bottom half of the garment, which
   is the same complaint one step quieter. The label gets its own chip instead:
   it reads on a white tee and on a black hoodie, and it covers only itself.
   Hover dims and pushes in on the image, which is where the affordance is. */
.st-cat-card .wp-block-cover__background {
	background-color: transparent !important;
	opacity: 1 !important;
	background-image: none;
}
.st-cat-card .wp-block-cover__image-background {
	transition: transform 0.6s cubic-bezier(0.2, 0.6, 0.2, 1), filter 0.3s ease;
}
.st-cat-card:hover .wp-block-cover__image-background,
.st-cat-card:focus-within .wp-block-cover__image-background {
	transform: scale(1.06);
	filter: brightness(0.88);
}
@media (prefers-reduced-motion: reduce) {
	.st-cat-card .wp-block-cover__image-background {
		transition: filter 0.3s ease;
	}
	.st-cat-card:hover .wp-block-cover__image-background,
	.st-cat-card:focus-within .wp-block-cover__image-background {
		transform: none;
	}
}
.st-cat-card__label {
	margin: 0;
}
/* The chip. inline-block so it hugs the words rather than banding the tile,
   and the ink colour is forced past the block's own has-ti-fg-alt-color —
   white text on a near-white chip was the previous version's failure mode. */
.st-cat-card__label a {
	display: inline-block;
	padding: 0.4rem 0.65rem;
	background: rgba(250, 250, 247, 0.94);
	color: var(--wp--preset--color--ti-fg);
	text-decoration: none;
}
/* ☠️ The stretch below is measured against the nearest POSITIONED ancestor,
   and that is not the card. Core's cover stylesheet ships
   `.wp-block-cover .wp-block-cover__inner-container{position:relative}`, and
   assets/tg-ghost-print.css restates it to lift the chip above the printed
   mark — so `inset:0` resolved to the label's own 147x35 box and the image
   above it was not a link at all. Nothing errors, the rule reads as correct,
   and the label still navigates, so it presents as "the tile just isn't
   clickable". Measured on the live home page 2026-09-09.

   Taking the inner container out of the flow fixes it. `z-index` survives:
   the container is a flex item of `.wp-block-cover` (display:flex in core),
   and z-index applies to flex items with no position — so tg-ghost-print's
   `z-index:2` still holds the chip over the ink.

   Doubled class on purpose. style.css is enqueued at functions.php:80 and
   tg-ghost-print at :209, so at an equal (0,2,0) the later sheet wins and
   this does nothing. (0,3,0) settles it whatever the order. */
.st-cat-card.st-cat-card .wp-block-cover__inner-container {
	position: static;
}
.st-cat-card__label a::after {
	content: "";
	position: absolute;
	inset: 0;
}
.st-cat-card:hover .st-cat-card__label a {
	background: var(--wp--preset--color--ti-fg);
	color: var(--wp--preset--color--ti-fg-alt);
}
.st-cat-card__label a:focus-visible::after {
	outline: 2px solid var(--wp--preset--color--ti-fg);
	outline-offset: -4px;
}

/* ── Shop Titan · Scoped product loop (st-best-sellers, st-product-grid) ────
   The [products] shortcode renders woocommerce/content-product.php — the
   store's own card — which is Tailwind-classed. functions.php loads the
   .dtla-legacy-scope-prefixed copy of the legacy bundle on these pages, so
   the card styles itself without the unscoped bundle reaching Global Styles.
   Only the grid frame lives here; the card's own look comes from that sheet. */
.st-product-loop .products {
	margin: 0;
	padding: 0;
	list-style: none;
}
.st-product-loop .woocommerce-result-count,
.st-product-loop .woocommerce-ordering {
	display: none;
}

/* ── Shop Titan · Block tabs (pattern: st-best-sellers) ─────────────────────
   assets/tabs.js builds the strip from each panel's first heading and adds
   .is-enhanced. Until then every panel is visible with its heading, which is
   what the editor and a no-JS visitor see.                                  */
.st-tabs__list {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: 0.5rem;
	margin-bottom: 2rem;
}
.st-tabs__tab {
	cursor: pointer;
	padding: 0.6rem 1.25rem;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: var(--wp--preset--font-size--small);
	letter-spacing: 0.06em;
	text-transform: uppercase;
	border: 1px solid var(--wp--preset--color--ti-line);
	transition: background-color 0.15s ease, color 0.15s ease;
}
/* The resting colours are a *default*, so they sit at zero specificity: a tab
   that opts into the site's Button element (`.wp-element-button`, which WP
   emits as `:root :where(…)` = 0,1,0) must be able to take those colours
   instead. A bare `.st-tabs__tab` here would tie with it and the winner would
   come down to stylesheet order. See § STYLING in the root CLAUDE.md.       */
:where(.st-tabs__tab) {
	color: var(--wp--preset--color--ti-fg);
	background: transparent;
}
.st-tabs__tab:hover {
	border-color: var(--wp--preset--color--ti-fg);
}
/* Scoped away from the pill skin, which gets its fill from Global Styles
   rather than from a token named here. At (0,2,0) this would outrank the
   element rule and the pill would silently stay ink-on-white.               */
.st-tabs:not( .st-tabs--pill ) .st-tabs__tab.is-active {
	color: var(--wp--preset--color--ti-fg-alt);
	background: var(--wp--preset--color--ti-fg);
	border-color: var(--wp--preset--color--ti-fg);
}
.st-tabs__tab:focus-visible {
	outline: 2px solid var(--wp--preset--color--ti-fg);
	outline-offset: 3px;
}
.st-tabs.is-enhanced > .st-tabs__panel:focus {
	outline: none;
}

/* ── Shop Titan · Product track (pattern: st-best-sellers) ──────────────────
   The scrolling is CSS scroll-snap, so swipe works with no JS at all;
   assets/track.js only adds the arrows and dots. The .is-track class is added
   by that script, so the grid stays a plain grid when it does not run.      */
.st-track .products.is-track {
	display: flex;
	flex-wrap: nowrap;
	gap: 1rem;
	margin: 0;
	padding: 0 0 0.5rem;
	list-style: none;
	overflow-x: auto;
	scroll-snap-type: x mandatory;
	scrollbar-width: none;
}
.st-track .products.is-track::-webkit-scrollbar {
	display: none;
}
.st-track .products.is-track > li {
	flex: 0 0 calc((100% - 4rem) / 5);
	margin: 0;
	scroll-snap-align: start;
}
@media (max-width: 1023px) {
	.st-track .products.is-track > li {
		flex-basis: calc((100% - 2rem) / 3);
	}
}
@media (max-width: 781px) {
	.st-track .products.is-track > li {
		flex-basis: calc((100% - 1rem) / 2);
	}
}
@media (max-width: 559px) {
	.st-track .products.is-track > li {
		flex-basis: 78%;
	}
}
.st-track__nav {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: 1.25rem;
	margin-top: 1.5rem;
}
.st-track__arrow,
.st-track__dot {
	cursor: pointer;
	background: transparent;
	color: var(--wp--preset--color--ti-fg);
	border: 1px solid var(--wp--preset--color--ti-line);
	transition: background-color 0.15s ease, opacity 0.15s ease;
}
.st-track__arrow {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.5rem;
	height: 2.5rem;
	padding: 0;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: 1.25rem;
	line-height: 1;
}
.st-track__arrow:hover:not(:disabled),
.st-track__dot:hover {
	background: var(--wp--preset--color--ti-fg);
	color: var(--wp--preset--color--ti-fg-alt);
}
.st-track__arrow:disabled {
	opacity: 0.35;
	cursor: default;
}
.st-track__dots {
	display: flex;
	align-items: center;
	gap: 0.5rem;
}
.st-track__dot {
	width: 0.6rem;
	height: 0.6rem;
	padding: 0;
	border-radius: 50%;
}
.st-track__dot.is-active {
	background: var(--wp--preset--color--ti-fg);
	border-color: var(--wp--preset--color--ti-fg);
}
.st-track__arrow:focus-visible,
.st-track__dot:focus-visible {
	outline: 2px solid var(--wp--preset--color--ti-fg);
	outline-offset: 3px;
}
@media (prefers-reduced-motion: reduce) {
	.st-track .products.is-track {
		scroll-behavior: auto;
	}
}

/* ── Shop Titan · Category gallery (pattern: st-category-gallery) ───────────
   Title always readable; the supporting line is revealed on hover and on
   keyboard focus, so it is reachable without a pointer.                     */
.st-gallery-card__desc {
	margin-top: 0.35rem;
	max-height: 0;
	opacity: 0;
	overflow: hidden;
	transition: max-height 0.25s ease, opacity 0.25s ease;
}
.st-gallery-card:hover .st-gallery-card__desc,
.st-gallery-card:focus-within .st-gallery-card__desc {
	max-height: 6rem;
	opacity: 1;
}
.st-gallery-card h3 a {
	text-decoration: none;
}
.st-gallery-card h3 a::after {
	content: "";
	position: absolute;
	inset: 0;
}
.st-gallery-card {
	position: relative;
}
@media (prefers-reduced-motion: reduce) {
	.st-gallery-card__desc {
		transition: none;
	}
}

/* ── Shop Titan · Category hero media (pattern: st-category-hero) ───────────
   Product shots are usually transparent PNGs that should sit whole rather
   than fill the column, so cap the height and centre it.                   */
.st-cat-hero-media .wp-block-image {
	margin: 0;
	display: flex;
	justify-content: center;
}
.st-cat-hero-media .wp-block-image img {
	max-height: 26rem;
	width: auto;
	object-fit: contain;
}

/* ── Shop Titan · Newsletter form (pattern: st-newsletter) ──────────────────
   A real form, so it is a Custom HTML block rather than blocks — the field
   and button are styled here to match the theme.json button element.       */
.st-subscribe {
	display: flex;
	flex-wrap: wrap;
	gap: 0.75rem;
	max-width: 34rem;
	margin-inline: auto;
}
.st-subscribe__field {
	flex: 1 1 16rem;
	min-width: 0;
	padding: 0.85rem 1rem;
	font-family: var(--wp--preset--font-family--helvetica);
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--ti-fg);
	background: var(--wp--preset--color--ti-bg-alt);
	border: 1px solid var(--wp--preset--color--ti-bg-alt);
	border-radius: 2px;
}
.st-subscribe__field:focus-visible {
	outline: 2px solid var(--wp--preset--color--ti-fg-alt);
	outline-offset: 2px;
}
.st-subscribe__submit {
	flex: 0 0 auto;
	cursor: pointer;
	padding: 0.85rem 1.5rem;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--wp--preset--color--ti-fg);
	background: var(--wp--preset--color--ti-bg-alt);
	border: 1px solid var(--wp--preset--color--ti-bg-alt);
	border-radius: 2px;
}
.st-subscribe__submit:disabled {
	opacity: 0.6;
	cursor: wait;
}
/* ── Shop Titan · Opt-in forms (patterns: st-sample-request, st-price-breaks)
   Same plumbing as .st-subscribe, two or three fields instead of one, and it
   has to read on both a light band and a dark one — so the field takes its
   colours from the section rather than assuming the dark newsletter band.  */
.st-optin {
	display: flex;
	flex-wrap: wrap;
	gap: 0.75rem;
	max-width: 34rem;
	margin-inline: auto;
}
.st-optin__field {
	flex: 1 1 14rem;
	min-width: 0;
	padding: 0.85rem 1rem;
	font-family: var(--wp--preset--font-family--helvetica);
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--ti-fg);
	background: var(--wp--preset--color--ti-bg-alt);
	border: 1px solid var(--wp--preset--color--ti-line);
	border-radius: 2px;
}
.st-optin__field:focus-visible {
	outline: 2px solid var(--wp--preset--color--ti-accent);
	outline-offset: 2px;
}
.st-optin--inverse .st-optin__field {
	border-color: var(--wp--preset--color--ti-bg-alt);
}
.st-optin--inverse .st-optin__field:focus-visible {
	outline-color: var(--wp--preset--color--ti-fg-alt);
}
.st-optin__submit {
	flex: 1 1 100%;
	cursor: pointer;
	padding: 0.85rem 1.5rem;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: var(--wp--preset--font-size--small);
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.06em;
	color: var(--wp--preset--color--ti-fg-alt);
	background: var(--wp--preset--color--ti-accent);
	border: 1px solid var(--wp--preset--color--ti-accent);
	border-radius: 2px;
}
.st-optin--inverse .st-optin__submit {
	color: var(--wp--preset--color--ti-fg);
	background: var(--wp--preset--color--ti-bg-alt);
	border-color: var(--wp--preset--color--ti-bg-alt);
}
.st-optin__submit:disabled {
	opacity: 0.6;
	cursor: wait;
}

.st-form__status {
	flex: 1 0 100%;
	margin: 0;
	min-height: 1.4em;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--ti-accent-secondary);
}
.st-form__status.is-success {
	color: var(--wp--preset--color--ti-fg-alt);
}
.st-form__status.is-error {
	color: var(--wp--preset--color--ti-line);
}
/* Honeypot — off-screen rather than display:none, so a bot that checks
   computed styles still fills it in. Never shown, never focusable. */
.st-form__hp {
	position: absolute;
	left: -9999px;
	width: 1px;
	height: 1px;
	opacity: 0;
	pointer-events: none;
}

/* ── Shop Titan · Search results (template: search.html) · TT-5911 B9 ───────
   The generic `/?s=` page: a title band, the mixed list WordPress's own
   Post Template block prints for the SITE'S main search query (`inherit:true`
   — unchanged, and out of scope: a product-only search already renders
   through the catalog template and its own kit dress, see
   templates/product-search-results.html and assets/st-catalog.css), and a
   closing "try another search" band on the ink surface with its own field.
   Tokens only; no query behaviour touched, CSS and spacing only.           */
.st-search-results-list .wp-block-post-template {
	list-style: none;
	margin: 0;
	padding: 0;
}
.st-search-results-list .wp-block-post-template > li {
	border-top: 1px solid var(--wp--preset--color--ti-line);
	padding-block: 1.25rem;
}
.st-search-results-list .wp-block-post-template > li:last-child {
	border-bottom: 1px solid var(--wp--preset--color--ti-line);
}
.st-search-results-list .wp-block-post-date {
	display: block;
	margin-bottom: 0.35rem;
	font-family: var(--wp--preset--font-family--space-mono);
	letter-spacing: var(--wp--custom--st-track);
	text-transform: var(--wp--custom--st-case);
	color: var(--wp--preset--color--ti-accent-secondary);
}
.st-search-results-list .wp-block-post-title {
	margin: 0;
}
.st-search-results-list .wp-block-post-title a {
	text-decoration: none;
}
.st-search-results-list .wp-block-post-title a:hover {
	text-decoration: underline;
}
.st-search-results-list .wp-block-post-featured-image {
	margin: 0 0 0.75rem;
}
.st-search-results-list .wp-block-post-featured-image img {
	aspect-ratio: var(--wp--custom--st-card-aspect);
	object-fit: contain;
	background: var(--wp--custom--st-panel);
	border-radius: var(--wp--custom--st-radius);
}

/* The field and button on the closing ink band — same recipe as
   .st-subscribe (a light field on the dark surface), applied to core's own
   search block classes instead of a custom-HTML form's. */
.st-search-again .wp-block-search {
	max-width: 34rem;
	margin-inline: auto;
}
.st-search-again .wp-block-search__inside-wrapper {
	border: 0;
}
.st-search-again .wp-block-search__input {
	min-height: var(--wp--custom--st-tap-target);
	padding-inline: 1rem;
	font-family: var(--wp--preset--font-family--helvetica);
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--ti-fg);
	background: var(--wp--preset--color--ti-bg-alt);
	border: 1px solid var(--wp--preset--color--ti-bg-alt);
	border-radius: var(--wp--custom--st-radius);
}
.st-search-again .wp-block-search__input:focus-visible {
	outline: 2px solid var(--wp--preset--color--ti-fg-alt);
	outline-offset: 2px;
}
.st-search-again .wp-block-search__button {
	min-height: var(--wp--custom--st-tap-target);
	padding-inline: 1.5rem;
	font-family: var(--wp--preset--font-family--space-mono);
	font-weight: 700;
	text-transform: var(--wp--custom--st-case);
	letter-spacing: var(--wp--custom--st-track);
	color: var(--wp--preset--color--ti-fg);
	background: var(--wp--preset--color--ti-bg-alt);
	border: 1px solid var(--wp--preset--color--ti-bg-alt);
	border-radius: var(--wp--custom--st-radius);
}

/* ── Shop Titan · Coupon cards (pattern: st-flash-deals) ────────────────────
   Perforated ticket edge on the code, to read as a voucher stub.          */
.st-coupon {
	height: 100%;
	padding: 2rem 1.5rem;
	border: 1px solid var(--wp--preset--color--ti-accent-secondary);
}
.st-coupon__amount {
	margin-bottom: 0.5rem;
	font-weight: 800;
	letter-spacing: -0.02em;
}
.st-coupon__code {
	margin-top: 1.25rem;
	padding: 0.6rem 1rem;
	border: 1px dashed var(--wp--preset--color--ti-fg-alt);
	letter-spacing: 0.16em;
	text-transform: uppercase;
}

/* ── Shop Titan · Woo reviews (pattern: st-reviews) ─────────────────────────
   The All Reviews block ships its own layout; only the surrounding rules
   are set here so it sits in the theme's hairline idiom.                   */
.st-reviews .wc-block-review-list-item__item {
	padding: 1.5rem 0;
	border-bottom: 1px solid var(--wp--preset--color--ti-line);
}
.st-reviews .wc-block-review-list-item__product {
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: var(--wp--preset--font-size--small);
}
.st-reviews .wc-block-review-list-item__author,
.st-reviews .wc-block-review-list-item__published-date {
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: var(--wp--preset--font-size--small);
	color: var(--wp--preset--color--ti-accent-secondary);
}

/* ── Shop Titan · Assurance list (pattern: st-premium-services) ─────────────*/
.st-assurances {
	margin: 0;
	padding-left: 1.1rem;
}
.st-assurances li {
	margin-bottom: 0.5rem;
}

/* ── Shop Titan · Link list (pattern: st-link-list) ─────────────────────────
   Hairline-ruled rows with a trailing arrow, in place of bullets.           */
.st-link-list {
	list-style: none;
	margin: 0;
	padding: 0;
}
.st-link-list li {
	border-bottom: 1px solid var(--wp--preset--color--ti-line);
}
.st-link-list li:first-child {
	border-top: 1px solid var(--wp--preset--color--ti-line);
}
.st-link-list li a {
	display: flex;
	align-items: baseline;
	gap: 1rem;
	padding: 0.9rem 0;
	text-decoration: none;
}
.st-link-list li a::after {
	content: "\2192"; /* → */
	margin-left: auto;
	color: var(--wp--preset--color--ti-accent-secondary);
	transition: transform 0.2s ease;
}
.st-link-list li a:hover {
	text-decoration: underline;
}
.st-link-list li a:hover::after {
	transform: translateX(3px);
}

/* ── DTLA · Site header (block: neve-fse-child/dtla-header) ─────────────────
   Port of the hello-elementor-child header (parts/blocks/header/header.php).
   Structure and behaviour follow the original; the styling is rebuilt on
   theme.json presets rather than the old Tailwind bundle, which is gated to
   the ported cart/checkout/product pages — see inc/header.php for why.

   Layout is one grid whose columns swap at the tablet breakpoint:
     desktop  [wide logo] [nav] [actions]
     mobile   [burger] [square logo] [actions]  + nav as a slide-in panel     */

/* The sticky header does not work without this. neve-fse ships
   `body,html{height:100%}` and `.wp-site-blocks{display:flex;height:100%}`, so
   the sticky header's containing block is exactly ONE VIEWPORT tall no matter
   how long the page is. The bar pins correctly for the first (100vh - header)
   pixels and then scrolls away for good — which reads as "sticky is broken"
   rather than "the parent box ended", and no rule on the header itself can fix
   it. Measured 2026-08-09 on a 10380px home page: .wp-site-blocks computed to
   900px, and the header unstuck at 799px.
   min-height keeps neve-fse's short-page footer-at-the-bottom behaviour. */
html,
body {
	height: auto;
}
.wp-site-blocks {
	height: auto;
	min-height: 100vh;
}

/* Sticky has to be applied to the template-part wrapper, not just the cover:
   templates render this part as <header class="wp-block-template-part">, and a
   sticky element only travels within its own parent's box — which here is
   exactly the height of the header, so it would unstick immediately. */
.wp-block-template-part:has(> .dtla-header-cover) {
	position: sticky;
	top: 0;
	z-index: 110;
}
.dtla-header-cover {
	position: sticky;
	top: 0;
	z-index: 110;
	background: var(--wp--preset--color--ti-bg);
	transition: transform 0.35s ease-in-out;
	margin-left: calc(var(--wp--custom--spacing--outer)*-1) !important;
	margin-right: calc(var(--wp--custom--spacing--outer)*-1) !important;
	width: unset;
	border-bottom: 1px solid var(--wp--preset--color--ti-line);
}
.dtla-header-cover.is-stuck {
	box-shadow: 0 1px 0 var(--wp--preset--color--ti-line);
}
/* Scrolling down past the fold hides the bar; scrolling up brings it back. */
.dtla-header-cover.is-hidden {
	transform: translateY(-100%);
}

/* ── Admin bar ──────────────────────────────────────────────────────────────
   The admin bar is position:fixed across the top of the viewport, so anything
   pinned at top:0 ends up underneath it. Offsets are WordPress's own: 32px
   above 782px, 46px at or below it, and at 600px and under the bar stops being
   fixed and scrolls away with the page — so the offset goes back to 0.
   Both the wrapper and the cover are listed because whichever of the two is
   actually doing the sticking depends on :has() support; the inner one clamps
   to its container, so setting both is safe.                                 */
.admin-bar .wp-block-template-part:has(> .dtla-header-cover),
.admin-bar .dtla-header-cover {
	top: 32px;
}
@media screen and (max-width: 782px) {
	.admin-bar .wp-block-template-part:has(> .dtla-header-cover),
	.admin-bar .dtla-header-cover {
		top: 46px;
	}
}
@media screen and (max-width: 600px) {
	.admin-bar .wp-block-template-part:has(> .dtla-header-cover),
	.admin-bar .dtla-header-cover {
		top: 0;
	}
}

/* ── Announcement bar ───────────────────────────────────────────────────────
   FLOOR (b): one line, never two, at a DECLARED height — stTopbarHeight, which
   the kit's own bar has carried since 🧢 kit-chrome. Before this the bar's
   height was an outcome of 0.55rem of padding on a 0.75rem/1.4 paragraph, so
   the sticky header's offset moved with the copy: measured on threadghost.com
   2026-09-30 the bar is 34.4px and the header row under it renders 61.7px at
   375 against the 56px token and 66.5px at 1536 against 64.

   Colour stays this theme's inverted surface rather than the kit's accent bar.
   The kit mixes an `--st-on-accent` it MEASURES per store; this theme has no
   such slot, and accent + ti-fg-alt is #ff6a00 under #eef1f3 on Safety — 2.87:1,
   a contrast failure. The port takes the kit's geometry and leaves its colour. */
.dtla-topbar {
	display: flex;
	align-items: center;
	justify-content: center;
	block-size: var(--wp--custom--st-topbar-height);
	background: var(--wp--preset--color--ti-bg-inv);
	color: var(--wp--preset--color--ti-fg-alt);
	padding-inline: 1.5rem;
	text-align: center;
}
.dtla-topbar__text {
	margin: 0;
	min-inline-size: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: 0.75rem;
	line-height: 1.4;
	letter-spacing: var(--wp--custom--st-track);
}
.dtla-topbar__code {
	appearance: none;
	background: none;
	border: 0;
	padding: 0 0 0 0.35rem;
	font: inherit;
	font-weight: 700;
	color: inherit;
	text-decoration: underline;
	text-underline-offset: 2px;
	cursor: pointer;
}
.dtla-topbar__code.is-copied {
	text-decoration: none;
	opacity: 0.7;
}

/* ── Header row ─────────────────────────────────────────────────────────────*/
/* FLOOR (a): a container is `min(maxWidth, 100% - 2 * gutter)` and never a
   fixed pixel width with its own padding. The row's HEIGHT is stHeaderHeight
   (56 below desktop, 64 from 1024), not whatever the logo plus 0.9rem of
   padding came to — which is the same defect the bar above had. */
.dtla-header {
	display: grid;
	grid-template-columns: auto 1fr auto;
	align-items: center;
	gap: 2rem;
	inline-size: 100%;
	max-inline-size: min(
		var(--wp--custom--st-shell),
		calc(100% - 2 * var(--wp--custom--st-gutter))
	);
	min-block-size: var(--wp--custom--st-header-height);
	margin-inline: auto;
	/* The row's height is the TOKEN, so it is not also padding. Stated as an
	   explicit zero: the rule it replaces was `padding: 0.9rem 1.5rem`, and a row
	   carrying both a min-block-size and block padding has two height rules with
	   no way to tell which one is meant. */
	padding-block: 0;
	padding-inline: 0;
}
.dtla-header__logo {
	display: flex;
	align-items: center;
	text-decoration: none;
}
.dtla-header__logo img {
	display: block;
	width: auto;
	height: 26px;
}
.dtla-header__logo--square,
.dtla-header__burger {
	display: none;
}
/* Shown when no logo is configured — see dtla_header_logo(). Sized to sit in
   the same slot as the image so the header height does not jump. */
.dtla-header__wordmark {
	font-family: var(--wp--preset--font-family--helvetica);
	font-size: 1.25rem;
	font-weight: 800;
	letter-spacing: -0.03em;
	line-height: 26px;
	white-space: nowrap;
	color: var(--wp--preset--color--ti-fg);
}
/* The wordmark is the link home, so FLOOR (g)'s 44px is its own and not just
   the nav's. 🧢 measured why: a 144x24 box beside five 44px ones left a 10px
   difference in top edges, and rubric A3 read it as a header nav on two rows. */
.dtla-header__logo {
	min-block-size: var(--wp--custom--st-tap-target);
	/* Both axes. The square slot's mark renders 33.9px wide, so height alone
	   left a control that was tall enough and still too narrow to hit —
	   measured at 375, 768 and 1024 in the candidate render. */
	min-inline-size: var(--wp--custom--st-tap-target);
	justify-content: center;
}

/* ── Navigation ─────────────────────────────────────────────────────────────*/
.dtla-header__nav-head {
	display: none;
}
.dtla-header__nav-list {
	display: flex;
	flex-wrap: nowrap;
	align-items: center;
	gap: 1.75rem;
	list-style: none;
	margin: 0;
	padding: 0;
}
.dtla-header__nav-item {
	position: relative;
}
/* FLOOR (g): 44px minimum on the row, and the tracking is stTrack — not a
   second 0.05em nobody can find. threadghost.com measured 74.2 x 22.4 here at
   1536; the kit's same link is 114.3 x 44. */
.dtla-header__nav-item > a {
	display: inline-flex;
	align-items: center;
	gap: 0.35rem;
	min-block-size: var(--wp--custom--st-tap-target);
	padding-block: 0;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: var(--wp--preset--font-size--small);
	text-transform: var(--wp--custom--st-case);
	letter-spacing: var(--wp--custom--st-track);
	text-decoration: none;
	white-space: nowrap;
}
.dtla-header__nav-item > a:hover {
	color: var(--wp--preset--color--ti-accent-secondary);
}
/* The kit's open-menu underline: 2px of accent under the item whose panel is
   open, the rule's own thickness (stRule), 200ms. It replaces nothing — the
   theme had no open state on the item at all, so an open dropdown looked the
   same as a hover on the one beside it. */
.dtla-header__nav-item::after {
	content: "";
	position: absolute;
	inset-inline: 0;
	bottom: 0;
	block-size: var(--wp--custom--st-rule);
	background: transparent;
	transition: background 0.2s ease-out;
}
.dtla-header__nav-item.is-open::after,
.dtla-header__nav-item.has-children:hover::after,
.dtla-header__nav-item.has-children:focus-within::after {
	background: var(--wp--preset--color--ti-accent);
}
/* Chevron for items with children. A CSS caret rather than the icon font, so
   the submenu still reads correctly if icons-style.css ever stops loading.   */
.dtla-header__nav-toggle {
	appearance: none;
	background: none;
	border: 0;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-block-size: var(--wp--custom--st-tap-target);
	min-inline-size: var(--wp--custom--st-tap-target);
	/* No BLOCK padding on a control that declares its own 44px: the box centres
	   its content, and padding on top of the minimum is what made the header row
	   grow from 96.1px to 99.4px in the first candidate render instead of
	   settling on stHeaderHeight. */
	padding-block: 0;
	padding-inline: 0.25rem;
	margin-left: 0.15rem;
	cursor: pointer;
	line-height: 0;
	color: inherit;
}
.dtla-header__nav-toggle::after {
	content: "";
	display: block;
	width: 6px;
	height: 6px;
	border-right: 1.5px solid currentColor;
	border-bottom: 1.5px solid currentColor;
	transform: translateY(-2px) rotate(45deg);
	transition: transform 0.2s ease;
}
.dtla-header__nav-item.is-open > .dtla-header__nav-toggle::after {
	transform: translateY(1px) rotate(225deg);
}
.dtla-header__submenu {
	list-style: none;
	margin: 0;
	padding: 0.6rem 0;
	min-width: 13rem;
}
.dtla-header__submenu a {
	display: block;
	padding: 0.45rem 1.1rem;
	font-size: var(--wp--preset--font-size--small);
	text-decoration: none;
	white-space: nowrap;
}
.dtla-header__submenu a:hover {
	background: var(--wp--preset--color--ti-bg-alt);
}

/* ── Actions ────────────────────────────────────────────────────────────────*/
.dtla-header__actions {
	display: flex;
	flex-wrap: nowrap;
	align-items: center;
	gap: 0.35rem;
}
/* FLOOR (g) again: measured 34.8 x 38.4 at 375 and 97 x 36.8 at 1536, so every
   action in the header missed on its height at every width. */
.dtla-header__action {
	position: relative;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 0.4rem;
	appearance: none;
	background: none;
	border: 0;
	min-block-size: var(--wp--custom--st-tap-target);
	min-inline-size: var(--wp--custom--st-tap-target);
	padding-block: 0;
	padding-inline: 0.6rem;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: var(--wp--preset--font-size--small);
	color: inherit;
	text-decoration: none;
	white-space: nowrap;
	cursor: pointer;
	transition: color 0.2s ease;
}
.dtla-header__action:hover {
	color: var(--wp--preset--color--ti-accent-secondary);
}
.dtla-header__action [class*="icon-"] {
	font-size: 20px;
	line-height: 1;
}
/* ── The primary action ─────────────────────────────────────────────────────
   The kit's header carries ONE action on the store's accent — the quote CTA —
   with the store's own corner (stRadius), and below the collapse point it drops
   to a 44x44 icon on the same accent. The theme had no primary at all: five
   identical text actions, none of which reads as the thing to press.

   WHICH action is primary is a SITE SETTING, not a literal here: an item in the
   Header Guest / Header Logged-in menu whose CSS Classes field contains `cta`
   gets this treatment (dtla_header_action_from_item). No store name, no label
   and no URL enters this repo. */
.dtla-header__action--cta {
	background: var(--wp--preset--color--ti-accent);
	color: var(--wp--preset--color--ti-fg-alt);
	border-radius: var(--wp--custom--st-radius);
	padding-inline: 1rem;
}
.dtla-header__action--cta:hover {
	color: var(--wp--preset--color--ti-fg-alt);
	filter: brightness(0.92);
}

.dtla-header__cart-count::before {
	content: "(";
}
.dtla-header__cart-count::after {
	content: ")";
}

/* ── Below the collapse width ───────────────────────────────────────────────
   The boundary is theme.json `custom.stHeaderCollapse`. It is 1240px because
   the widest store header MEASURES 1221px on one line at the 24px gutter
   (ThreadGhost, in a browser, 2026-10-01: logo 190 + nav 491 + actions 428 +
   two 32px gaps + gutters), rounded up to the next 20. It was 1160 while the
   row was computed from the monospace advance; #66 widened it with 44px targets
   and the store's tracking and the wordmark was clipped between 1160 and 1220.
   Before that, the old 1024 left a 117px band where the desktop nav did not fit
   — Michael, 2026-09-25 V1 04:12: "our header is all messed up". Every header
   breakpoint in this file, in
   assets/st-nav-mega.css and in assets/mega-menu.js is that one value, and
   tools/floor-check.py fails the build if one of them drifts.
   PROVISIONAL: the 📐 floor spec adopts or revises this number; it does not
   write a second one.                                                        */
@media (max-width: 1239.98px) {
	.dtla-header {
		gap: 0.75rem;
		/* Same rule under the collapse point: the row is stHeaderHeight (56 here),
		   and 0.7rem of block padding on top of it rendered 11.2px of dead space
		   above a 44px control at 375, 768 and 1024. */
		padding-block: 0;
		padding-inline: 1rem;
	}
	.dtla-header__logo--wide {
		display: none;
	}
	.dtla-header__logo--square {
		display: flex;
		justify-self: center;
	}
	.dtla-header__logo--square img {
		height: 40px;
	}

	/* Burger takes the first column; the nav leaves the grid entirely. */
	/* FLOOR (g): measured 32.8 x 26.3 on threadghost.com at 375, 768 and 1024 —
	   the single control every phone visitor has to hit first. `align-items`
	   keeps the three bars 20px wide inside a 44px box. */
	.dtla-header__burger {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		gap: 4.5px;
		appearance: none;
		background: none;
		border: 0;
		min-block-size: var(--wp--custom--st-tap-target);
		min-inline-size: var(--wp--custom--st-tap-target);
		padding-block: 0;
		cursor: pointer;
		color: inherit;
	}
	.dtla-header__burger span {
		display: block;
		width: 20px;
		height: 1.5px;
		background: currentColor;
		transition: transform 0.2s ease, opacity 0.15s ease;
	}
	.dtla-header-cover.is-open .dtla-header__burger span:nth-child(1) {
		transform: translateY(6px) rotate(45deg);
	}
	.dtla-header-cover.is-open .dtla-header__burger span:nth-child(2) {
		opacity: 0;
	}
	.dtla-header-cover.is-open .dtla-header__burger span:nth-child(3) {
		transform: translateY(-6px) rotate(-45deg);
	}

	/* Slide-in panel. Off-canvas rather than display:none so it animates and so
	   the links stay focusable in source order.                               */
	.dtla-header__nav {
		position: fixed;
		top: 0;
		left: 0;
		bottom: 0;
		width: min(20rem, 85vw);
		z-index: 120;
		background: var(--wp--preset--color--ti-bg);
		border-right: 1px solid var(--wp--preset--color--ti-line);
		overflow-y: auto;
		transform: translateX(-100%);
		transition: transform 0.3s ease;
	}
	.dtla-header-cover.is-open .dtla-header__nav {
		transform: translateX(0);
	}
	/* The off-canvas panel is fixed too, so it needs the same clearance. */
	.admin-bar .dtla-header__nav {
		top: 32px;
	}
	@media screen and (max-width: 782px) {
		.admin-bar .dtla-header__nav {
			top: 46px;
		}
	}
	@media screen and (max-width: 600px) {
		.admin-bar .dtla-header__nav {
			top: 0;
		}
	}
	.dtla-header__nav-head {
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 1rem 1.25rem;
		border-bottom: 1px solid var(--wp--preset--color--ti-line);
		font-family: var(--wp--preset--font-family--space-mono);
		font-size: var(--wp--preset--font-size--small);
		text-transform: uppercase;
		letter-spacing: 0.08em;
	}
	.dtla-header__nav-close {
		appearance: none;
		background: none;
		border: 0;
		display: inline-flex;
		align-items: center;
		justify-content: center;
		/* FLOOR (g): measured 22 x 24 — the smallest control on the store. */
		min-block-size: var(--wp--custom--st-tap-target);
		min-inline-size: var(--wp--custom--st-tap-target);
		padding: 0 0.25rem;
		font-size: 1.5rem;
		line-height: 1;
		cursor: pointer;
		color: inherit;
	}
	.dtla-header__nav-list {
		flex-direction: column;
		align-items: stretch;
		gap: 0;
		padding: 0.5rem 0;
	}
	.dtla-header__nav-item {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
	}
	.dtla-header__nav-item > a {
		flex: 1;
		padding: 0.75rem 1.25rem;
	}
	.dtla-header__nav-toggle {
		padding: 0.75rem 1.25rem;
	}
	/* Inside the panel the item is the row, so the underline above would draw a
	   2px accent line across every row of an open submenu. */
	.dtla-header__nav-item::after {
		content: none;
	}
	.dtla-header__submenu {
		flex: 1 0 100%;
		padding: 0 0 0.5rem 1.25rem;
	}

	/* The scrim must sit BELOW .dtla-header-cover (110), not between it and the
	   panel. The panel's z-index: 120 is scoped inside the cover's own stacking
	   context, so it cannot out-rank a sibling of the cover at any value — a
	   scrim at 115 paints over the open menu, not behind it. */
	.dtla-header__overlay {
		position: fixed;
		inset: 0;
		z-index: 109;
		background: rgba(10, 10, 10, 0.45);
	}
	.dtla-no-scroll,
	.dtla-no-scroll body {
		overflow: hidden;
	}

	/* Labels give way to icons; the cart count keeps its parentheses. */
	.dtla-header__action-label {
		position: absolute;
		width: 1px;
		height: 1px;
		margin: -1px;
		padding: 0;
		overflow: hidden;
		clip-path: inset(50%);
		white-space: nowrap;
	}
	.dtla-header__action [class*="icon-"] {
		font-size: 22px;
	}
}

@media (max-width: 600px) {
	.dtla-header__logo--square img {
		height: 34px;
	}
	.dtla-header__actions {
		gap: 0;
	}
	.dtla-header__action {
		padding: 0.45rem 0.4rem;
	}
}

/* ── Desktop submenu ────────────────────────────────────────────────────────
   Above the collapse width the submenu is a hover/focus dropdown. Kept out of
   the block above so the mobile panel is not fighting absolute positioning.  */
@media (min-width: 1240px) {
	/* THE KIT'S DROPDOWN, ported. 🧢 replaced a 13rem box hanging off the item
	   with a panel the width of the header, its links on FLOOR (c)'s own grid
	   mechanism — `repeat(auto-fill, minmax(200px, 1fr))` — so a twelve-child
	   menu drops to two columns and then one instead of squeezing twelve 60px
	   links. 200px is the kit's number and is deliberately NOT a token: FLOOR
	   (c)'s table is about SECTIONS and has no row for a menu panel, so the two
	   layers carry the same literal with the same comment rather than inventing
	   a floor number the rubric cannot judge.

	   `inset-inline: 0` resolves against .dtla-header-cover, which is
	   position: sticky and therefore the nearest positioned ancestor. */
	.dtla-header__submenu {
		position: absolute;
		top: 100%;
		inset-inline: 0;
		display: grid;
		grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
		column-gap: 1.5rem;
		inline-size: 100%;
		max-inline-size: none;
		box-sizing: border-box;
		padding-block: 2rem;
		padding-inline: max(
			var(--wp--custom--st-gutter),
			calc((100% - var(--wp--custom--st-shell)) / 2)
		);
		background: var(--wp--preset--color--ti-bg);
		border: 0;
		border-bottom: 1px solid var(--wp--preset--color--ti-line);
		opacity: 0;
		visibility: hidden;
		transform: translateY(4px);
		transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
	}
	/* The panel is the header's, not the item's — the item only triggers it. */
	.dtla-header__submenu a {
		min-block-size: var(--wp--custom--st-tap-target);
		display: flex;
		align-items: center;
		padding-inline: 0;
	}
	.dtla-header__submenu a:hover {
		background: none;
		opacity: 0.6;
	}
	.dtla-header__nav-item:hover > .dtla-header__submenu,
	.dtla-header__nav-item:focus-within > .dtla-header__submenu,
	.dtla-header__nav-item.is-open > .dtla-header__submenu {
		opacity: 1;
		visibility: visible;
		transform: translateY(0);
	}
}

/* ── Navigation · core/navigation build ─────────────────────────────────────
   The block form of the primary nav, rendered when the store has a
   wp_navigation post — see dtla_header_nav_ref() in inc/header.php. The classic
   menu markup above is untouched and still renders when it does not.

   Every selector here is anchored on BOTH .dtla-header__nav and
   .dtla-header__nav-block, giving three classes. That is deliberate: core's own
   block-library rules are two (`.wp-block-navigation .wp-block-navigation-item__content`)
   and Neve FSE ships navigation styles too, so a two-class rule here is a TIE
   decided by stylesheet order — and woocommerce-style.css already proves this
   theme cannot rely on winning that. Three classes wins on specificity instead.

   Do not add `!important` in this block. If a rule loses, it is because a
   selector lost a class, not because it needs more weight.                    */
.dtla-header__nav .dtla-header__nav-block {
	/* Core prints its own font-size/line-height on the <nav>; the items below
	   set their own, and inheriting these would move the whole bar. */
	font-size: inherit;
	line-height: inherit;
}

/* Core wraps the list in a responsive container unless overlayMenu is "never".
   It is pinned to "never" in dtla_header_nav_block(), so this should never
   match — it is here so a store that flips that attribute in the editor gets a
   flattened wrapper rather than a second collapsed menu inside our panel. */
.dtla-header__nav .dtla-header__nav-block .wp-block-navigation__responsive-container {
	position: static;
	background: none;
	padding: 0;
}
.dtla-header__nav .dtla-header__nav-block .wp-block-navigation__responsive-container-open,
.dtla-header__nav .dtla-header__nav-block .wp-block-navigation__responsive-container-close {
	display: none;
}

.dtla-header__nav .dtla-header__nav-block .wp-block-navigation__container {
	display: flex;
	flex-wrap: nowrap;
	align-items: center;
	gap: 1.75rem;
	list-style: none;
	margin: 0;
	padding: 0;
}
.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-item {
	position: relative;
	margin: 0;
}
/* The kit's open-menu underline, on the block form: stRule of accent under the
   item whose panel is open. Only inside the row — the phone panel turns it off
   below the collapse point, where every item is a full-width row. */
@media (min-width: 1240px) {
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-submenu::after {
		content: "";
		position: absolute;
		inset-inline: 0;
		bottom: 0;
		block-size: var(--wp--custom--st-rule);
		background: transparent;
		transition: background 0.2s ease-out;
	}
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-submenu:hover::after,
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-submenu:focus-within::after,
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-submenu:has( > .wp-block-navigation-submenu__toggle[aria-expanded="true"] )::after {
		background: var(--wp--preset--color--ti-accent);
	}
}
/* FLOOR (g): 44px and stTrack, the same two numbers the classic menu above
   now carries. This is the form ThreadGhost actually renders (it has a
   wp_navigation post), and it measured 74.2 x 22.4 at 1536 and 275.4 x 43.2
   in the phone panel — the second missing 44 by 0.8px. */
.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-item__content {
	display: inline-flex;
	align-items: center;
	gap: 0.35rem;
	min-block-size: var(--wp--custom--st-tap-target);
	padding-block: 0;
	padding-inline: 0;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: var(--wp--preset--font-size--small);
	text-transform: var(--wp--custom--st-case);
	letter-spacing: var(--wp--custom--st-track);
	text-decoration: none;
	white-space: nowrap;
	color: var(--wp--preset--color--ti-fg);
}
.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-item__content:hover {
	color: var(--wp--preset--color--ti-accent-secondary);
	text-decoration: none;
}

/* The chevron. Core renders a real <button> with an inline SVG, which is kept
   rather than swapped for the classic menu's CSS caret: it carries the
   aria-expanded state core's own view script drives, and being inline it cannot
   disappear the way an icon font can. */
.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-submenu__toggle {
	appearance: none;
	background: none;
	border: 0;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-block-size: var(--wp--custom--st-tap-target);
	/* HELD BACK from #66 (2026-10-01, WB header row): the 44px MINIMUM WIDTH made
	   each of the four chevrons 42px wider than its 11px glyph, so the row needs
	   ~1400px while stHeaderCollapse said 1160 and the logo was squeezed and
	   clipped between 1024 and ~1380. The height stays 44. The collapse token is now
	   1240 (enough for the 11px chevron); a 44px-wide chevron would need ~1420, so
	   restore it only together with a further re-measure of that token. */
	padding-block: 0;
	padding-inline: 0.25rem;
	margin-left: 0.15rem;
	cursor: pointer;
	line-height: 0;
	color: inherit;
}
.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-submenu__toggle svg {
	/* With the toggle's width minimum held back (above), the button is only as wide
	   as its padding plus its content, and an SVG in a flex box shrinks to fit: the
	   11px arrow rendered 1.6px wide, so the dropdown cue disappeared. */
	flex-shrink: 0;
}
.dtla-header__nav .dtla-header__nav-block .wp-block-navigation__submenu-icon svg {
	width: 0.7em;
	height: 0.7em;
	transition: transform 0.2s ease;
}
.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-submenu__toggle[aria-expanded="true"] svg {
	transform: rotate(180deg);
}

.dtla-header__nav .dtla-header__nav-block .wp-block-navigation__submenu-container {
	list-style: none;
	margin: 0;
	padding: 0.6rem 0;
	min-width: 13rem;
	border: 0;
}
.dtla-header__nav .dtla-header__nav-block .wp-block-navigation__submenu-container .wp-block-navigation-item__content {
	display: block;
	padding: 0.45rem 1.1rem;
	text-transform: none;
	letter-spacing: 0;
	font-family: var(--wp--preset--font-family--helvetica);
}

/* Below the collapse width: the block list becomes the off-canvas panel's
   stack. Same boundary as the classic menu above and as assets/mega-menu.js —
   theme.json `custom.stHeaderCollapse`. */
@media (max-width: 1239.98px) {
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation__container {
		flex-direction: column;
		align-items: stretch;
		gap: 0;
		padding: 0.5rem 0;
	}
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-item {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
	}
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-item__content {
		flex: 1;
		padding: 0.75rem 1.25rem;
	}
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-submenu__toggle {
		padding: 0.75rem 1.25rem;
	}
	/* Core positions submenus absolutely at every width. Inside a 20rem
	   off-canvas panel that puts them off the edge of the screen, so they are
	   returned to flow and revealed by the toggle's own aria-expanded state —
	   which is the attribute core's view script already maintains. */
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation__submenu-container {
		position: static;
		flex: 1 0 100%;
		width: auto;
		min-width: 0;
		padding: 0 0 0.5rem 1.25rem;
		background: none;
		opacity: 1;
		visibility: visible;
		transform: none;
		display: none;
	}
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-submenu:has( > .wp-block-navigation-submenu__toggle[aria-expanded="true"] ) > .wp-block-navigation__submenu-container {
		display: block;
	}
}

/* Above the collapse width: hover/focus dropdown, matching the classic
   menu's chrome. */
@media (min-width: 1240px) {
	/* THE KIT'S DROPDOWN on the block form — the same panel, the same grid and
	   the same 200px literal as the classic menu's rule above. Core positions
	   this container absolutely and gives it `min-width: 13rem`; both are
	   replaced here rather than layered over, which is why `left` becomes
	   `inset-inline` and the width is the header's.

	   `box-sizing` is stated because core's block-library sets `padding: 0.5rem
	   0` on the container and the panel's own padding has to live inside the
	   header's width, not add to it. */
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation__submenu-container {
		position: absolute;
		top: 100%;
		inset-inline: 0;
		left: auto;
		display: grid;
		grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr));
		column-gap: 1.5rem;
		inline-size: 100%;
		min-inline-size: 0;
		max-inline-size: none;
		box-sizing: border-box;
		padding-block: 2rem;
		padding-inline: max(
			var(--wp--custom--st-gutter),
			calc((100% - var(--wp--custom--st-shell)) / 2)
		);
		background: var(--wp--preset--color--ti-bg);
		border: 0;
		border-bottom: 1px solid var(--wp--preset--color--ti-line);
		opacity: 0;
		visibility: hidden;
		transform: translateY(4px);
		transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s;
	}
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation__submenu-container .wp-block-navigation-item__content {
		display: flex;
		align-items: center;
		min-block-size: var(--wp--custom--st-tap-target);
		padding: 0;
	}
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-item:hover > .wp-block-navigation__submenu-container,
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-item:focus-within > .wp-block-navigation__submenu-container,
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-submenu:has( > .wp-block-navigation-submenu__toggle[aria-expanded="true"] ) > .wp-block-navigation__submenu-container {
		opacity: 1;
		visibility: visible;
		transform: translateY(0);
	}
}

/* THE DROPDOWN IS THE HEADER'S WIDTH, not the width of the item it hangs from
   (TT-5911, Diego 2026-10-01: Products opened as a 656px strip clipped off the
   left edge of the screen, x=-324 at 1440; the kit's is a full-width panel).
   Two things were wrong, both measured on the live page:
   1. core gives every nav item `position: relative`, so the panel's
      `inset-inline: 0` resolved against the 94px item, not the header. The nav
      row's wrappers go static so the panel's containing block is the sticky
      header cover, which is the full width.
   2. core's rule for an expanded submenu (`width: auto`, five classes) outranks
      the panel's `inline-size: 100%`, and inc/st-nav-mega's `width: max-content`
      sized the long menus to their content. One selector at six classes sets
      the width and the kit's three-column row flow for every dropdown.
   Columns are 20rem so three items do not spread across a 1920 screen. */
@media (min-width: 1240px) {
	.dtla-header__nav .dtla-header__nav-block,
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation__container,
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-item {
		position: static;
	}
	.dtla-header__nav .dtla-header__nav-block .wp-block-navigation-item.has-child > .wp-block-navigation__submenu-container.wp-block-navigation__submenu-container {
		width: 100%;
		max-width: none;
		grid-auto-flow: row;
		grid-template-rows: none;
		grid-template-columns: repeat(3, minmax(0, 20rem));
	}
}

/* ── Shop Titan · Mega menu panels, block-built ─────────────────────────────
   Panels converted from the ported PHP partials to block template parts
   (parts/mega-*.html, rendered by dtla_mega_menu_render_panel()).

   These deliberately do NOT carry the legacy `.mega-menu` class, so none of
   the ported Tailwind card styling reaches them. The only thing inherited from
   the legacy layer is the cover's positioning and the show/hide rule, which
   keys off [data-mega-menu-content] rather than any class.                   */
.st-mega-panel {
	position: relative;
	width: 100%;
	background: var(--wp--preset--color--ti-bg);
	border-bottom: 1px solid var(--wp--preset--color--ti-line);
}
.st-mega-panel__eyebrow {
	margin: 0 0 1.5rem;
}

.st-mega-service {
	padding: 0.9rem 1rem;
	border: 1px solid transparent;
	border-radius: 2px;
	transition: border-color 0.2s ease, background-color 0.2s ease;
}
.st-mega-service:hover {
	border-color: var(--wp--preset--color--ti-line);
	background: var(--wp--preset--color--ti-bg-alt);
}
.st-mega-service__title {
	margin: 0 0 0.35rem;
	font-weight: 700;
	letter-spacing: -0.01em;
}
/* Stretch the title link over the whole card so the entire tile is the
   target, rather than just the few words of the heading. */
.st-mega-service__title a {
	text-decoration: none;
}
.st-mega-service__title a::after {
	content: "";
	position: absolute;
	inset: 0;
}
.st-mega-service {
	position: relative;
}
.st-mega-service__desc {
	margin: 0;
	line-height: 1.5;
}

/* Badge ("New", "Popular") — the menu's label_text field. */
.st-mega-service__badge {
	display: inline-block;
	margin-left: 0.5rem;
	padding: 0.1rem 0.45rem;
	border: 1px solid var(--wp--preset--color--ti-line);
	border-radius: 999px;
	font-family: var(--wp--preset--font-family--space-mono);
	font-size: 0.625rem;
	font-weight: 400;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	vertical-align: middle;
	color: var(--wp--preset--color--ti-accent-secondary);
}

/* Child links, when a service has sub-items in the menu. */
.st-mega-service__links {
	list-style: none;
	margin: 0.6rem 0 0;
	padding: 0;
}
.st-mega-service__links li {
	margin: 0;
}
.st-mega-service__links a {
	display: block;
	padding: 0.2rem 0;
	text-decoration: none;
	color: var(--wp--preset--color--ti-accent-secondary);
}
.st-mega-service__links a:hover {
	color: var(--wp--preset--color--ti-fg);
	text-decoration: underline;
}
/* The card-wide stretched link would otherwise sit on top of these and swallow
   their clicks — lift them back above it. */
.st-mega-service__links {
	position: relative;
	z-index: 1;
}

/* ── Lazily-loaded legacy popups ────────────────────────────────────────────
   assets/header.js drops the ported contact / search popups into this
   container along with build/css/*.scoped.css, whose selectors are all
   confined to it (tasks/scope-legacy-css.mjs).                               */
.dtla-legacy-scope .popups.hidden {
	display: none;
}

/* The container must generate NO box.
   tasks/scope-legacy-css.mjs rewrites the old theme's `body` / `html` / `:root`
   rules onto .dtla-legacy-scope so body-anchored popup rules keep working. That
   also hands this wrapper <div> body's own background and min-height — which
   rendered as a full viewport of painted empty space at the bottom of every
   page. `display: contents` keeps the scope and the custom properties (both
   inherit fine) while removing the box entirely. The popups and the mega menu
   cover are position:fixed, so they are unaffected by having no parent box.
   !important because the generated sheet is injected after style.css and its
   :not() selectors out-specify a bare class. */
.dtla-legacy-scope {
	display: contents !important;
}

/* ── DTLA · Product archive — room, hierarchy and polish ────────────────────
   ⚠️ The archive MARKUP is Diego's: inc/woocommerce/archive.php builds the
   shell, woocommerce/content-product.php builds the card, and
   src/scss/woocommerce/archive.scss + src/scss/kit/cards/product.scss size
   both. Nothing here edits any of those files. This block re-sizes and
   re-orders from our own stylesheet, so his layer stays the single source of
   the DOM and deleting this block restores his design exactly.

   ── WHY EVERY SELECTOR CARRIES `body#dtla_body_id.dtla__body` ──
   Both of his stylesheets are nested under `#dtla_body_id.dtla__body`, which
   `body_class` and the `wp_body_open` script put on the <body> of a product
   archive (functions.php:652 and :680). An id cannot be out-ranked by any
   number of classes, so the first version of this block — plain class
   selectors — lost every layout declaration silently and only its hover
   states survived. These rules repeat his anchor and add the `body` element
   on top: (1,3,1) against his (1,3,0). Won on specificity, not on
   !important and not on load order, so it stays legible in devtools.

   What it overrides, so it can be found and undone:
     archive.scss:70        ul.products             grid-template-columns
     archive.scss:89        .has-filter-widget      grid-template-columns 20% 1fr
     container-width.scss:2 :root                   --container-max-width
     product.scss:10        .dtla_product           flex order of the card
     product.scss:110       .size-woocommerce_thumbnail / .kover2  fit + ratio

   Measured before (1512px viewport, /custom/t-shirts/): content 1337,
   sidebar 267, loop 1046, card 246, product image 244x291. On a blanks
   catalog the photograph IS the product, and 246px of it was not selling. */

/* A 20% sidebar is 267px of a 1337px row. The widget inside it is a handful
   of checkboxes at any width — it does not need a fifth of the page, and
   every pixel it gives back goes into the photograph.

   ⚠️ 210px was measured on ThreadGhost, whose filter terms are one short word
   ("Hoodies", "Bags"). It does not survive a taxonomy with real names.

   Measured on the LIVE rail (safetyuniformco /products/, 2026-09-21), the
   binding constraint turned out not to be the rail width at all but the chrome
   nested inside it — see § "One panel, not two" and § "Reclaim the toggle
   gutter" below. With both of those fixed the term `<ul>` is 206px of the 240
   and exactly one term of 33 still wraps ("FR / Flame-Resistant", the longest
   label in the taxonomy). At the old 210 the same `<ul>` would be 176.

   240 costs the grid 30px, which at four-across is 7px per card — below the
   threshold the original 210 measurement was protecting. Do not push further
   without re-measuring the card; and if long labels regress, check the nested
   chrome before reaching for more width, because that is what it was last time.
*/
@media (min-width: 1024px) {
	body#dtla_body_id.dtla__body .dtla-shop-archive__content.has-filter-widget {
		grid-template-columns: 240px minmax(0, 1fr);
		column-gap: 2.5rem;
	}
}

/* ── Full width to a 1080p window ────────────────────────────────────────────
   Michael, 2026-09-23 20:26: *"we can see we're not using the full page width.
   And so we really want to let these products shine as much as possible."*
   Diego 22:39: *"it's safe to say, at this point in history, that they're using
   at least 1080p… that's probably going to be the width, if we do full width,
   and that's the one that we could optimize for the most."* Michael 24:01:
   *"anything above this maybe should be constrained to this."*

   So the catalog and the product page run to the GLOBAL wide size and stop
   there. `theme.json` sets that token to 1920px — a 1080p window — and this
   rule reads the token rather than restating the number, so moving it is one
   edit in one file and never fifty per-store stylesheets. `max-width` is a cap,
   so below 1920 the container is simply fluid, which is Diego's *"it has to be
   responsive, that's a given"* at 23:35.

   The var is declared on :root, so setting it on <body> re-inherits into
   `.container` without touching the rule that reads it. It was 1680px above
   1600px and the theme's 1548 cap below, which is the "not using the full page
   width" he was looking at. */
body#dtla_body_id.dtla__body.post-type-archive-product,
body#dtla_body_id.dtla__body.tax-product_cat,
body#dtla_body_id.dtla__body.tax-product_tag,
body#dtla_body_id.dtla__body.single-product {
	--container-max-width: var(--wp--style--global--wide-size, 1920px);
}

/* Cards get WIDER, not more numerous. Four across is the right density for a
   garment judged by eye; auto-fill would have spent the new width on a fifth
   column and handed the gain straight back. */
@media (min-width: 1280px) {
	body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell ul.products {
		gap: 1.75rem 1.5rem;
	}
}

/* ── The image well ──────────────────────────────────────────────────────
   His card crops to `object-fit: cover` at 0.84/1. On flat garment shots
   photographed on white that quietly clips sleeves and hems, and it clipped
   them differently on every product because the source crops are not
   consistent. `contain` inside a fixed 4/5 well cannot crop a garment, and
   the letterboxing is invisible because the well is the same white the
   photographs were shot on. The padding is what makes it read as a catalog
   plate rather than a thumbnail. */
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .product_thumbnail_wrapper__container {
	width: 100%;
	background: var(--wp--preset--color--ti-bg-alt, #fff);
}
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .product_thumbnail_wrapper__container img,
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .kover2 {
	width: 100%;
	max-height: none;
	/* COVER and no padding since 2026-10-02 (Diego: "the images on the product
	   cards are getting cut off unless they are hovered over, we probably don't
	   need them to be contained like that"). The plate above was `contain` with
	   6% padding, so a square photograph sat small inside the 4/5 well with a
	   band of panel round it, while the hover photograph filled the well: the
	   card's picture changed size on hover. The trade the note above warns of is
	   real and accepted: a square photograph loses about a tenth on each side.
	   CORRECTION, same day: the photograph was square only because the card was
	   handed WooCommerce's square CROP. inc/catalog-archive.php now asks for the
	   uncropped size, which is close to 4/5, so `cover` here trims almost nothing. */
	aspect-ratio: auto;
	object-fit: cover;
	padding: 0;
}

/* .kover1 is the alternate shot that fades in on hover. It is absolutely
   positioned, so it has to track the well it sits in rather than carry its
   own 1/1.3 ratio, or the two images swap at different sizes. */
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .kover1 {
	aspect-ratio: auto;
	transition: opacity 0.45s ease;
}

/* ── Reading order ───────────────────────────────────────────────────────
   The card renders swatches, then "SKU: 5001", then the product name. That is
   the order the WooCommerce hooks fire in (inc/woocommerce/product.php:137
   and :146 both hang off woocommerce_shop_loop_item_title, at priorities 6
   and 7, ahead of the title at 10) — it is inventory order, not shopping
   order, and it buried the name of the product under a part number.

   `order` on the flex children fixes the presentation without touching a
   hook or a template. Name first, then the style number as a quiet caption,
   then colors, then price on the baseline. Only one element in the card is
   focusable, so tab order is unaffected by this. */
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product > a.woocommerce-loop-product__link {
	order: 1;
}
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product > .woocommerce-loop-product__title {
	order: 2;
}
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product > .dtla-meta-box {
	order: 3;
}
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product > .dtla-color-box {
	order: 4;
}
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product > .price {
	order: 5;
}
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product > .add_to_cart_button,
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product > .added_to_cart {
	order: 6;
}

/* ── Type hierarchy ──────────────────────────────────────────────────────
   Name, style number and price all rendered at 14px, so nothing led. */
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .woocommerce-loop-product__title {
	margin: 1.1rem 0 0.15rem;
	font-size: 0.95rem;
	font-weight: 500;
	line-height: 1.4;
	min-height: 2.8em;
	color: var(--wp--preset--color--ti-fg, #0a0a0a);
}

/* The style number is how a buyer who already knows AS Colour searches, so it
   stays on the card — as a caption, in the mono face the rest of the site
   uses for specs, not as the first thing they read. */
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .dtla-meta-box {
	margin: 0 0 0.75rem;
}
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .dtla-sku-box {
	font-family: var(--wp--preset--font-family--space-mono, ui-monospace, monospace);
	font-size: 0.68rem;
	letter-spacing: 0.08em;
	line-height: 1.2;
	color: var(--wp--preset--color--ti-accent-secondary, #6b6b6b);
}

/* Price is the second thing a buyer looks for and it was set at the weight of
   body copy. */
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .price > span {
	font-size: 1rem;
	font-weight: 700;
	line-height: 1.3;
}

/* Swatches: 24px circles with a 2px ring wrapped to a second row on any
   product carrying more than about eight colors, which made the price sit at
   a different height on every card in the row. Smaller circles fit the common
   case on one line; the row is capped so a 20-color style cannot push the
   price out of alignment, and the "+N" the card already prints is what tells
   the buyer there is more. */
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .dtla-color-box {
	margin: 0 0 1rem;
	gap: 0.15rem;
	max-height: 1.5rem;
	overflow: hidden;
}
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .dtla-color-box .kcolor_box__wrapper {
	width: 1.25rem;
	height: 1.25rem;
}

/* ── Card behaviour ──────────────────────────────────────────────────────
   Lift the card and push into the photograph. `transition: all` is already on
   li.product from the legacy bundle, so only the new properties are stated. */
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product {
	overflow: hidden;
	transition: transform 0.28s ease, box-shadow 0.28s ease;
}
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product:hover {
	transform: translateY(-4px);
	box-shadow: 0 16px 34px -20px rgba(10, 10, 10, 0.5);
}
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .product_thumbnail_wrapper {
	overflow: hidden;
}
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .product_thumbnail_wrapper__container img {
	transition: transform 0.6s cubic-bezier(0.2, 0.6, 0.2, 1);
}
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product:hover .product_thumbnail_wrapper__container img {
	transform: scale(1.04);
}

/* The card's one focusable element is the image link, and its focus ring was
   being clipped by the overflow:hidden above. Put a visible ring on the card
   itself instead — WCAG 2.4.11, and it is the only way to see where you are
   tabbing through a 4-across grid. */
body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product:focus-within {
	outline: 2px solid var(--wp--preset--color--ti-fg, #0a0a0a);
	outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
	body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product,
	body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .product_thumbnail_wrapper__container img,
	body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product .kover1 {
		transition: none;
	}
	body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product:hover {
		transform: none;
	}
	body#dtla_body_id.dtla__body .dtla-shop-archive__loop-shell li.product:hover .product_thumbnail_wrapper__container img {
		transform: none;
	}
}

/* ── Catalog grid variations — the rules WordPress cannot carry ─────────────
   The six looks live in styles/catalog-*.json as block style variations, and
   each is a `styles.css` string that WP re-emits scoped to the block instance
   (`:root :where(.wp-block-woocommerce-product-collection.is-style-catalog-
   editorial--1 …)`).

   ⚠️ That processor only understands FLAT, SINGLE-SELECTOR rules. Two things
   it does silently, both caught in the generated CSS on /grid-styles/:

     @media(…){ & .a{…} }   the @media wrapper is DROPPED and the rules inside
                            are emitted UNCONDITIONALLY. A prefers-reduced-
                            motion block therefore disables the animation for
                            everybody — the exact inverse of its purpose, and
                            invisible unless you read the generated CSS.
     & .a,& .b{…}           mangled to `:root :where(scope){.a,}` — garbage,
                            and it takes the following rule down with it.

   So anything conditional belongs here instead. `:root :where(…)` scores
   (0,1,0), so a two-class selector out-specifies it; the !important is only
   needed because the variation declarations carry !important themselves. */

/* One focusable card in a grid of four is impossible to track without a ring. */
.st-catalog-grid .wc-block-product-template > li:focus-within {
	outline: 2px solid var(--wp--preset--color--ti-fg, #0a0a0a);
	outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
	.st-catalog-grid .wc-block-product-template > li,
	.st-catalog-grid .wc-block-components-product-image,
	.st-catalog-grid .wc-block-components-product-image img,
	.st-catalog-grid .wc-block-components-product-button .wp-block-button__link,
	.st-catalog-grid .st-catalog-grid__btn,
	.st-catalog-grid .wp-block-post-title a {
		transition: none !important;
	}
	.st-catalog-grid .wc-block-product-template > li:hover {
		transform: none !important;
	}
	.st-catalog-grid .wc-block-product-template > li:hover .wc-block-components-product-image img {
		transform: none !important;
	}
}

/* A coarse pointer never fires :hover, so any state parked behind it is
   unreachable on a phone. Nothing here is load-bearing — the hover states are
   emphasis, not information — but the image must not sit mid-zoom. */
@media (hover: none) {
	.st-catalog-grid .wc-block-product-template > li:hover {
		transform: none !important;
	}
	.st-catalog-grid .wc-block-product-template > li:hover .wc-block-components-product-image img {
		transform: none !important;
	}
}

/* ── Category archives: the WooCommerce header block ────────────────────────
   `<header class="woocommerce-products-header">` carries the H1 and, on page 1
   of a term archive, `.term-description` — WooCommerce's own output, rendered
   above the loop. Nothing in either stylesheet styled it, so ~1,600 characters
   of prose landed at the full container width in the default face, and the
   products started below it.

   Half of that was fixed as data: the seven-method list and the closing
   promise were byte-identical across all 11 category descriptions, so they
   moved into templates/taxonomy-product_cat.html and now render once, below
   the loop. Each description is roughly half the length and entirely unique
   to its category.

   The rest is measure. Prose set across 1,680px is unreadable regardless of
   how good it is. */
.woocommerce-products-header {
	margin-bottom: 2rem;
}

.woocommerce-products-header .term-description {
	/* The measure is a token now — theme.json `custom.stMeasure`. Row WB-254. */
	max-width: var(--wp--custom--st-measure, 68ch);
	margin-top: 0.9rem;
	font-size: 0.95rem;
	line-height: 1.65;
	color: var(--wp--preset--color--ti-accent-secondary, #55554f);
}

/* WooCommerce runs the description through wpautop, which turns the single
   blank-line breaks into one <p> full of <br>. Paragraphs therefore have to be
   spaced off the line break rather than off a margin that is not there. */
.woocommerce-products-header .term-description p {
	margin: 0 0 0.9rem;
}
.woocommerce-products-header .term-description p:last-child {
	margin-bottom: 0;
}
.woocommerce-products-header .term-description br {
	content: "";
	display: block;
	height: 0.9rem;
}

.woocommerce-products-header .term-description a {
	color: inherit;
	text-decoration: underline;
	text-underline-offset: 0.2em;
}
.woocommerce-products-header .term-description a:hover {
	color: var(--wp--preset--color--ti-fg, #0a0a0a);
}


/* ── Archive width: the padding stacks twice ────────────────────────────────
   Measured at 1512px on /custom/t-shirts/: 1497 viewport, and the products
   row was 1087 of it. The chain that ate the difference is

     .wp-site-blocks   48px each side   (parent theme root gutter)
     main.container    32px each side   (this theme's own gutter)

   which is 160px of horizontal padding before a single product is drawn, for
   one gutter's worth of visual effect. Handing the sidebar 57px back bought
   the card only 5px because of it.

   ⚠️ Do NOT fix this by shrinking `.wp-site-blocks` padding. Every full-bleed
   element on the page derives its negative margin from that value
   (`_global.scss:278` computes `calc(((100vw - var(--container-max-width)) / -2)
   - var(--container-padding-x))`). Dropping it 48 → 16 pushed `.dtla-header-cover`,
   `.dtla-topbar` and every alignfull group 32px past the viewport on both
   sides and put a horizontal scrollbar on the page. Measured, not guessed.

   Pull the grid out instead. Only this one section moves, the full-bleed maths
   everywhere else is untouched, and the page keeps a 32px gutter. */
@media (min-width: 1024px) {
	body#dtla_body_id.dtla__body .prod-archive-grid.dtla-shop-archive {
		margin-left: -32px;
		margin-right: -32px;
	}
}

/* ── The category description was the whole first screen ────────────────────
   At 1512 the description ran 301px tall in a 575px column with the entire
   right half of the page empty beside it, and the first row of products
   started below 640px. Two columns use the dead space and halve the height,
   which is the difference between seeing one row of products on open and
   seeing none. Single column below 1024 where there is no space to give. */
@media (min-width: 1024px) {
	.woocommerce-products-header .term-description {
		max-width: 1180px;
		column-count: 2;
		column-gap: 3.5rem;
	}
	.woocommerce-products-header .term-description p {
		margin-top: 0;
	}
}

/* ── Shop Titan · Alternating text/media row ────────────────────────────────
   The rhythm the corporate-merch redesign runs on: text one side, picture the
   other, flipping down the page. Authored media-first so that the single
   column a phone gets is always picture-then-text; `--text-first` swaps them
   back with `order` once there are two columns, which is the only place the
   flip is allowed to happen. Authoring some rows text-first would make the
   mobile stack alternate too, and a reader scrolling a phone reads that as
   two different components rather than one rhythm.

   The hero is the one deliberate exception: it is authored body-first, so a
   phone gets the h1 before the visual. Its image is portrait and full width
   there, and leading with it would push the headline most of a screen down.
   Body-first already puts the text in the left column, so it needs no
   `--text-first`.                                                           */
.st-split {
	display: grid;
	gap: 2rem;
}
.st-split__media img {
	display: block;
	width: 100%;
	height: auto;
}
@media (min-width: 782px) {
	.st-split {
		grid-template-columns: 1fr 1fr;
		column-gap: clamp(2.5rem, 5vw, 5rem);
		align-items: center;
	}
	.st-split--text-first > .st-split__media {
		order: 2;
	}
}

/* ── Shop Titan · Vertical tabs (assets/tabs.js, `st-tabs--vertical`) ───────
   Three columns: section heading far left, the stacked tab strip in the
   middle, the active panel on the right. The strip is generated, so a panel
   added in the editor gets its tab for free and the two can never disagree.

   Only `.is-enhanced` carries the grid. Without JS — and inside the editor
   canvas — every panel stays visible with its own heading, so the section is
   still fully editable and still reads as a plain stack of sub-sections.

   Below 900px it is a real accordion rather than a strip above a panel:
   `display:contents` dissolves the generated list so its buttons become flex
   items alongside the panels, and the `--st-order` pairs tabs.js writes drop
   each panel directly under its own tab.                                    */
@media (min-width: 900px) {
	.st-vtabs.is-enhanced {
		display: grid;
		grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr) minmax(0, 1.3fr);
		column-gap: clamp(2rem, 4vw, 4rem);
		align-items: start;
	}
}
.st-vtabs .st-tabs__list {
	display: flex;
	flex-direction: column;
	flex-wrap: nowrap;
	justify-content: flex-start;
	gap: 0;
	margin-bottom: 0;
	border-top: 1px solid var(--wp--preset--color--ti-line);
}
.st-vtabs .st-tabs__tab {
	display: block;
	width: 100%;
	text-align: left;
	padding: 1.15rem 1.25rem;
	border: 0;
	border-bottom: 1px solid var(--wp--preset--color--ti-line);
	background: transparent;
	color: var(--wp--preset--color--ti-fg);
	font-family: inherit;
	font-size: 1.0625rem;
	letter-spacing: 0;
	text-transform: none;
	transition: background-color 0.18s ease, color 0.18s ease;
}
.st-vtabs .st-tabs__tab:hover {
	background: color-mix(in srgb, var(--wp--preset--color--ti-fg) 7%, transparent);
}
.st-vtabs .st-tabs__tab.is-active {
	background: var(--wp--preset--color--ti-bg-inv);
	color: var(--wp--preset--color--ti-fg-alt);
	border-bottom-color: var(--wp--preset--color--ti-bg-inv);
}
.st-vtabs .st-tabs__panel {
	display: flex;
	flex-direction: column;
	gap: 1.15rem;
}
/* ⚠️ Any author `display` on a panel silently defeats `[hidden]`. tabs.js
   switches panels with the hidden property, whose `display:none` lives in the
   UA stylesheet, and an author rule beats the UA sheet whatever the
   specificity. The rule above shipped without this one and every panel stayed
   on screen at once: the tab strip highlighted correctly, `panel.hidden` was
   true, and nothing errored. The horizontal `.st-tabs` never hit it because it
   sets no display on its panels.                                            */
.st-vtabs .st-tabs__panel[hidden] {
	display: none;
}
.st-vtabs .st-tabs__panel img {
	display: block;
	width: 100%;
	height: auto;
}
@media (max-width: 899px) {
	.st-vtabs.is-enhanced {
		display: flex;
		flex-direction: column;
	}
	/* Dissolve the strip so the buttons and the panels are siblings and
	   `order` can interleave them. The list's own top rule goes with it, so
	   the first tab carries the rule instead.                               */
	.st-vtabs.is-enhanced > .st-tabs__list {
		display: contents;
	}
	.st-vtabs.is-enhanced .st-tabs__tab,
	.st-vtabs.is-enhanced > .st-tabs__panel {
		order: var(--st-order);
	}
	.st-vtabs.is-enhanced .st-tabs__tab:first-child {
		border-top: 1px solid var(--wp--preset--color--ti-line);
	}
	.st-vtabs.is-enhanced > .st-tabs__panel {
		padding: 1.5rem 1.25rem 2rem;
		border-bottom: 1px solid var(--wp--preset--color--ti-line);
	}
}

/* ── Shop Titan · FAQ rail ──────────────────────────────────────────────────
   Madeline asked for the vertical-tab treatment a second time on this page.
   The answer is to restyle the accordion that is already there, not to ship a
   second component: `wp:details` is what the `wpseo_schema_graph` filter in
   functions.php reads to emit FAQPage schema, so replacing it with a tab set
   would silently drop the page's structured data.

   The details blocks are authored with no inline style attributes for the same
   reason the catalog looks are: an inline style cannot be beaten from a
   stylesheet without `!important`, and a reskin has to be able to reach this. */
.st-faq-rail {
	display: grid;
	gap: 1.5rem;
}
@media (min-width: 900px) {
	.st-faq-rail {
		grid-template-columns: minmax(0, 0.8fr) minmax(0, 2.3fr);
		column-gap: clamp(2rem, 4vw, 4rem);
		align-items: start;
	}
}
.st-faq-rail__list > .wp-block-details {
	border-bottom: 1px solid var(--wp--preset--color--ti-line);
}
.st-faq-rail__list > .wp-block-details:first-child {
	border-top: 1px solid var(--wp--preset--color--ti-line);
}
.st-faq-rail__list > .wp-block-details > summary {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 1.5rem;
	padding: 1.15rem 1.25rem;
	cursor: pointer;
	list-style: none;
	font-size: 1.0625rem;
	transition: background-color 0.18s ease, color 0.18s ease;
}
.st-faq-rail__list > .wp-block-details > summary::-webkit-details-marker {
	display: none;
}
.st-faq-rail__list > .wp-block-details > summary::after {
	content: "+";
	font-family: var(--wp--preset--font-family--space-mono);
	flex: 0 0 auto;
}
.st-faq-rail__list > .wp-block-details[open] > summary::after {
	content: "\2212";
}
.st-faq-rail__list > .wp-block-details > summary:hover {
	background: color-mix(in srgb, var(--wp--preset--color--ti-fg) 7%, transparent);
}
.st-faq-rail__list > .wp-block-details[open] > summary {
	background: var(--wp--preset--color--ti-bg-inv);
	color: var(--wp--preset--color--ti-fg-alt);
}
.st-faq-rail__list > .wp-block-details > :not(summary) {
	padding: 1.25rem 1.25rem 1.75rem;
}
.st-faq-rail__list > .wp-block-details > summary:focus-visible {
	outline: 2px solid var(--wp--preset--color--ti-fg);
	outline-offset: -2px;
}

/* ── Shop Titan · Ethereal swatch stack (inline SVG) ────────────────────────
   The "consistent brand color" graphic. Drawn rather than photographed, and
   drawn in tokens rather than hexes, so a store that reskins `theme.json`
   gets its own version of it with no new asset. The glow is a real Gaussian
   blur on a duplicate of each card, which is what keeps it bleeding past the
   card edge the way the reference does instead of reading as a drop shadow. */
.st-ethereal {
	display: block;
	width: 100%;
	height: auto;
}
.st-ethereal__card {
	fill: var(--wp--preset--color--ti-fg);
}
.st-ethereal__stroke {
	fill: none;
	stroke: var(--wp--preset--color--ti-fg);
	stroke-width: 2;
	stroke-linecap: round;
	stroke-linejoin: round;
}
/* The drift sits on an outer <g> that carries no transform attribute of its
   own. A CSS transform on the isometric group would replace that attribute
   rather than compose with it, and the whole stack would snap flat.         */
@media (prefers-reduced-motion: no-preference) {
	.st-ethereal__drift {
		animation: st-ethereal-drift 6s ease-in-out infinite alternate;
	}
}
@keyframes st-ethereal-drift {
	from {
		transform: translateY(-7px);
	}
	to {
		transform: translateY(7px);
	}
}

/* ── Shop Titan · Corporate merch, second review pass (2026-08-13) ───────────
   Four notes from the 12 Aug screen recording. The transcript is the source;
   each rule below says which line it answers.                                */

/* "This hero image should not go above the heading."
   `.st-split` centres its two columns, and the hero visual is a tall portrait
   against a short text block, so centring pushed its top edge above the eyebrow
   by about 60px. The hero tops the two columns out together instead. Only the
   hero: every other row on the page is a wide image against a similar-height
   paragraph, where centring is right.                                        */
@media (min-width: 782px) {
	.st-split--hero {
		align-items: start;
	}
}

/* "The image should go above and the text should go below."
   Said of a Finishes panel, which was authored heading, line, picture. The
   panels now ship picture before line; this keeps the rule true even if a
   panel is re-authored in the editor, where the generated tab strip does not
   exist and the blocks are dragged around by hand.                           */
.st-vtabs .st-tabs__panel > figure {
	order: -1;
}

/* ── Pill tabs, the TwelveLabs "Tailored for your industry" shape ────────────
   A centred row of pills, the active one solid, the rest outlined. Same
   `.st-tabs` component and the same generated strip as everywhere else, so
   this is a skin rather than a second tab set: `st-tabs--pill` only changes
   how the buttons look.                                                      */
.st-tabs--pill .st-tabs__list {
	gap: 0.75rem;
	margin-bottom: 2.5rem;
}
/* Hold the lead to the same measure as the panel below it, or the section has
   two different column widths stacked on each other and reads as two things. */
.st-merch-tabs__head {
	max-width: 860px;
	margin-inline: auto;
}
/* Every declaration the Button element also sets is restated here at (0,2,0),
   because the active pill wears `.wp-element-button` and would otherwise take
   the button's weight (700) and line-height (inherit → 1.6) on selection only
   — which resizes the pill under the pointer and shifts the whole strip. Only
   colour is meant to come from Global Styles; shape and type stay ours.     */
.st-tabs--pill .st-tabs__tab {
	padding: 0.7rem 1.4rem;
	border-radius: 999px;
	border: 1px solid var(--wp--preset--color--ti-line);
	font-family: inherit;
	font-size: 1rem;
	font-weight: 400;
	line-height: normal;
	letter-spacing: 0;
	text-transform: none;
}
.st-tabs--pill .st-tabs__tab:hover {
	border-color: var(--wp--preset--color--ti-fg);
}
/* No background or colour here on purpose. assets/tabs.js puts
   `.wp-element-button` on the active pill, so its fill and text follow
   Site Editor → Styles → Colors → Elements → Button and change with it.
   The border is stated, and stays ink to match the buttons — the fill is what
   marks the pill active, so the outline can be constant across both states. */
.st-tabs--pill .st-tabs__tab.is-active {
	border-color: var(--wp--preset--color--ti-fg);
}
/* Same trap as the vertical set: an author `display` on a panel beats the
   user-agent's `[hidden] { display:none }` whatever the specificity, and every
   panel renders at once. See § STYLING in the root CLAUDE.md.               */
.st-tabs--pill .st-tabs__panel {
	display: grid;
	gap: 1.5rem;
	max-width: 1000px;
	margin-inline: auto;
	align-items: center;
}
.st-tabs--pill .st-tabs__panel[hidden] {
	display: none;
}
/* Picture and line share a row rather than stacking. Full width, the image ran
   most of a screen tall and pushed the line off the bottom of the window, so
   the tab did not read as one thing you could take in at a glance.

   The panel keeps its own max-width and the image a fixed ratio, so five tabs
   of very different subjects still change over at exactly the same height and
   the pills never move under the pointer.                                    */
.st-tabs--pill .st-tabs__panel img {
	display: block;
	width: 100%;
	height: auto;
	aspect-ratio: 4 / 3;
	object-fit: cover;
}
@media (min-width: 782px) {
	.st-tabs--pill .st-tabs__panel {
		grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
		column-gap: clamp(2rem, 4vw, 3.5rem);
	}
}
/* Below that it is one column and the picture leads, which is both the review's
   "image above, text below" and the rule the rest of the page follows.       */
@media (max-width: 781px) {
	.st-tabs--pill .st-tabs__panel img {
		aspect-ratio: 16 / 10;
	}
}

/* "Turn this into a GIF and have the colors change. This will become the dark
   grey, then this will become the light grey, and this will become the black,
   and they will rotate through all the different colours."
   Three cards holding three tones, each card handing its tone to the next, so
   over one cycle every card has been every tone. Staggering one animation by a
   third of its duration is what makes that a rotation rather than a blink.

   The tones live here as opacities of `ti-fg`, so a store that reskins gets its
   own three tones for free. Frozen for anyone who asked for less motion, on the
   pose the graphic used to hold.                                             */
@media (prefers-reduced-motion: no-preference) {
	.st-ethereal__card {
		animation: st-ethereal-tone 6s ease-in-out infinite;
	}
	/* Each card is alone inside its own transform <g>, so every one of them is
	   :nth-of-type(1) and a positional selector silently matches all three and
	   staggers none. They carry explicit classes for that reason.

	   ⚠️ The two delays are exact thirds of the duration and have to stay that
	   way. Change the 6s and these must follow, or the three cards stop being
	   evenly spaced round the cycle and the tones read as a blink rather than
	   as a rotation.                                                        */
	.st-ethereal__card--b { animation-delay: -2s; }
	.st-ethereal__card--c { animation-delay: -4s; }
}
@keyframes st-ethereal-tone {
	0%,  26%  { opacity: 1; }
	33%, 59%  { opacity: 0.55; }
	66%, 92%  { opacity: 0.25; }
	100%      { opacity: 1; }
}

/* ── Shop Titan · Corporate merch, third review pass (2026-08-13) ────────────
   Everything here answers a line from the second screen recording. The five
   notes that turn on a light blue are not built: no PMS number exists yet, and
   the home and about pages both promise "Monochrome only. No loud colors,
   ever." That is a decision, not a task.                                     */

/* "The hero graphic is still below the fold. Needs to be even with this block
   of text. In this area."
   Top-aligning fixed where it starts; it was still 641px tall, so on anything
   shorter than a 900px viewport the bottom ran off the screen. Capping it near
   the height of the text column puts the whole graphic in the band she drew,
   and the vh term keeps it above the fold on a laptop as well as a desktop. */
@media (min-width: 782px) {
	.st-split--hero .st-split__media img {
		width: auto;
		max-width: 100%;
		max-height: min(480px, calc(100vh - 16rem));
		margin-inline: auto;
	}
}

/* "This section feels too stuffy. Please use industry standards for there to be
   more breathing room."
   The head, the strip and the panel were on the section's default rhythm, which
   is tuned for a column of paragraphs, not for three stacked bands of different
   weight. Each gap grows with the viewport rather than being a fixed number, so
   a phone does not inherit desktop air it has no room for.                   */
.st-merch-tabs__head {
	margin-bottom: clamp(2rem, 4vw, 3.25rem);
}
.st-tabs--pill .st-tabs__list {
	margin-bottom: clamp(2.5rem, 5vw, 4rem);
}
.st-merch-tabs .st-tabs__panel {
	padding-block: clamp(0.5rem, 2vw, 1.5rem);
}

/* "Make this image smaller so we're able to read the text above the fold."
   The Finishes panel image was square in a ~430px column, so it was 430 tall
   and pushed its own caption past the bottom of the window. A 4:3 crop takes a
   quarter off the height and keeps the tab set changing over at one height. */
.st-vtabs .st-tabs__panel img {
	aspect-ratio: 4 / 3;
	object-fit: cover;
}

/* "Make this gradient less strong so it's not a clear line dividing."
   There was no gradient at all: two flat section colours meeting, which is the
   hardest edge available. The section now fades its own ground into the one
   below over the last third of its height, so the boundary has nowhere to draw
   a line. background-image sits over background-color, so this needs no
   !important against the preset class the block attribute emits.            */
.st-soft-edge {
	background-image: linear-gradient(
		to bottom,
		var(--wp--preset--color--ti-bg-alt) 62%,
		var(--wp--preset--color--ti-bg) 100%
	);
}

/* The dot field runs under this section's copy, so hold the text off the very
   edge of the canvas and keep it above the field. `.tg-halftone__copy` does the
   stacking; this is only the breathing room around it.                       */
.tg-halftone:has( .st-merch-tabs ) {
	padding-inline: 1.25rem;
}

/* ── st-auth · the log in / create account split ─────────────────────────────

   Form on Paper, full-height ink panel beside it. The section is a flow-layout
   group in the markup and becomes a two-track grid here, so it degrades to two
   stacked blocks on narrow screens with no media-query gymnastics.

   The doubled `.st-auth.st-auth` is not decoration. WordPress prints its flow
   layout rules INLINE in the body, after this stylesheet, so a tie on
   specificity is lost on source order. Two classes puts us out of reach.     */
.st-auth.st-auth {
	/* Every alignfull section here picks up a 48px inline gutter (24px on a
	   phone). That is right for a section of text and wrong for a split panel:
	   it leaves a strip of Paper running down the outside of the ink, which
	   reads as a rendering fault rather than as a margin. Each column carries
	   its own padding, so the copy is still held off the edge.               */
	padding-inline: 0;
}

.st-auth.st-auth > * {
	/* `:where(.is-layout-flow) > *` scores (0,0,0) but still applies a 24px
	   block margin, which inside a grid becomes a visible seam between the two
	   panels rather than the vertical rhythm it was meant to be.              */
	margin-block: 0;
}

@media ( min-width: 900px ) {
	.st-auth.st-auth {
		display: grid;
		grid-template-columns: 1fr 1fr;
		align-items: stretch;
		min-height: 640px;
	}
}

.st-auth__form-col,
.st-auth__brand {
	display: flex;
	flex-direction: column;
	justify-content: center;
}

/* ☠️ Without this every child of those columns shrink-wraps to its own text.
   WordPress's constrained layout centres children with `margin-inline: auto`,
   and an auto inline margin on a flex item CANCELS the default stretch, so the
   item resolves to fit-content instead of filling the column. The failure is
   quiet and asymmetric: the sign-up panel measured 420px because its privacy
   paragraph is long, while the login panel next to it measured 290px, so the
   form visibly changed width when you switched between them and neither one
   was the width the pattern asked for. `width: 100%` restores the fill; the
   constrained layout's own max-width still caps it and the auto margins still
   centre it.                                                                 */
.st-auth__form-col > *,
.st-auth__brand > * {
	width: 100%;
}

.st-auth__mark {
	opacity: 0.9;
}

/* ── st-auth · the forms ──────────────────────────────────────────────────────

   These are hand-built rather than WooCommerce's own form markup, so nothing
   here inherits from the legacy bundle — `/login/` is not an account page, so
   that stylesheet never loads. Everything the fields need is stated once.    */
.st-auth-form {
	max-width: 420px;
}

.st-auth-form__panel[hidden] {
	/* assets/auth.js switches panels with the `hidden` property, and
	   `[hidden] { display: none }` lives in the USER-AGENT stylesheet — any
	   author `display` on this element beats it at any specificity, because
	   that is an origin fight and author always wins. Nothing above sets
	   `display` on a panel today; this is here so that adding one later cannot
	   silently render both forms at once. Same trap as `.st-vtabs`.           */
	display: none;
}

.st-auth-form__field {
	margin: 0 0 1.25rem;
}

.st-auth-form__field label {
	display: block;
	margin-bottom: 0.4rem;
	font-family: var( --wp--preset--font-family--space-mono );
	font-size: 0.72rem;
	letter-spacing: 0.1em;
	text-transform: uppercase;
	color: var( --wp--preset--color--ti-accent-secondary );
}

.st-auth-form__input {
	width: 100%;
	padding: 0.8rem 0.9rem;
	border: 1px solid var( --wp--preset--color--ti-line );
	border-radius: 2px;
	background: var( --wp--preset--color--ti-bg-alt );
	color: var( --wp--preset--color--ti-fg );
	font-family: inherit;
	font-size: 1rem;
	line-height: 1.4;
}

.st-auth-form__input:focus {
	outline: 2px solid var( --wp--preset--color--ti-fg );
	outline-offset: 1px;
	border-color: var( --wp--preset--color--ti-fg );
}

.st-auth-form__row {
	display: flex;
	flex-wrap: wrap;
	gap: 0.75rem 1rem;
	align-items: center;
	justify-content: space-between;
	margin: 0 0 1.5rem;
}

.st-auth-form__check {
	display: inline-flex;
	align-items: center;
	gap: 0.5rem;
	font-size: 0.9rem;
	color: var( --wp--preset--color--ti-accent-secondary );
}

.st-auth-form__checkbox {
	width: 1rem;
	height: 1rem;
	accent-color: var( --wp--preset--color--ti-fg );
}

.st-auth-form__actions {
	margin: 0;
}

/* Two classes, so this out-specifies the (0,1,0) Styles → Elements → Button
   rule for LAYOUT only. Colour, type and padding are deliberately left to that
   rule, which is what keeps this button reskinning with every other one on the
   site instead of drifting into its own palette.                             */
.st-auth-form .st-auth-form__submit {
	display: block;
	width: 100%;
	border: 0;
	text-align: center;
	text-decoration: none;
	cursor: pointer;
}

.st-auth-form__link {
	color: var( --wp--preset--color--ti-fg );
	font-size: 0.9rem;
	text-underline-offset: 0.2em;
}

.st-auth-form__switch,
.st-auth-form__note {
	margin: 1.5rem 0 0;
	font-size: 0.9rem;
	color: var( --wp--preset--color--ti-accent-secondary );
}

.st-auth-form__heading {
	margin: 0 0 1.5rem;
	font-size: 1.35rem;
	line-height: 1.2;
	letter-spacing: -0.01em;
	color: var( --wp--preset--color--ti-fg );
}

/* WooCommerce prints this itself on `woocommerce_register_form`, so the class
   is theirs, not ours. Unstyled it renders at full body size and dominates a
   panel that is only four controls tall.                                     */
.st-auth-form .woocommerce-privacy-policy-text,
.st-auth-form__fine {
	margin: 0 0 1.25rem;
	font-size: 0.78rem;
	line-height: 1.5;
	color: var( --wp--preset--color--ti-accent-secondary );
}

.st-auth-form .woocommerce-privacy-policy-text p {
	margin: 0;
	font-size: inherit;
}

/* WooCommerce's own notice markup, which is how every login and sign-up error
   reaches the visitor. Unstyled it is an invisible bare list on this page.   */
.st-auth-form .woocommerce-error,
.st-auth-form .woocommerce-message,
.st-auth-form .woocommerce-info {
	margin: 0 0 1.5rem;
	padding: 0.9rem 1rem;
	border-left: 3px solid var( --wp--preset--color--ti-fg );
	background: var( --wp--preset--color--ti-bg-alt );
	list-style: none;
	font-size: 0.9rem;
}

/* An error has to be unmistakable, and this palette has no red in it by
   design. So it inverts instead of colouring: ink ground, On-ink text. Same
   two tokens every other dark surface uses, no new slot, and it still reskins
   with the store.                                                            */
.st-auth-form .woocommerce-error {
	background: var( --wp--preset--color--ti-bg-inv );
	border-left-color: var( --wp--preset--color--ti-fg );
	color: var( --wp--preset--color--ti-fg-alt );
}

.st-auth-form .woocommerce-error a {
	color: var( --wp--preset--color--ti-fg-alt );
}

.st-auth-form .woocommerce-error li,
.st-auth-form .woocommerce-message li,
.st-auth-form .woocommerce-info li {
	margin: 0;
	list-style: none;
}

/* ── Shop Titan · Corporate merch, fourth review pass (2026-08-18) ───────────
   Notes from the third screen recording. The blue is On-ink, `ti-fg-alt`,
   which is the Pantone she said would be provided and is already in the
   palette.                                                                  */

/* "Soften the top line so it has more of an ombre effect and lighten it a
   little bit around where there's text so the text is more visible."
   Nothing to zero over the first tenth, then held well back through the top
   third, which is the band holding the heading, the lead and the pill strip.
   Full strength only once it is past the copy.                              */
/* ⚠️ `.st-merch-tabs.tg-halftone` matches nothing. `tg-halftone` is on the
   SECTION and `st-merch-tabs` on the copy wrapper inside it, so the compound
   selector asks for one element wearing both and there is no such element.
   The canvas is a child of the section, so the property has to land there. */
/* "Where there is text, there should not be dots, or they should be very light
   so they're not visible behind the buttons."
   The heading, the lead and the pill strip together run to about half the
   section's height, so the field is held near nothing that whole way and only
   reaches full strength once it is past them. The earlier ramp was already at
   62% opacity by the time it hit the pills.                                  */
.tg-halftone:has( .st-merch-tabs ) {
	--tg-halftone-mask: linear-gradient(
		to bottom,
		transparent 0 14%,
		rgba( 0, 0, 0, 0.06 ) 30%,
		rgba( 0, 0, 0, 0.12 ) 46%,
		rgba( 0, 0, 0, 0.45 ) 62%,
		#000 78%
	);
}

/* "This section should resemble this section right here with an animation of
   the photo that hovers up." — the TwelveLabs Search & Discover list.
   `tabs.js` switches panels with the `hidden` property, so a panel goes from
   display:none to laid out, and an element that becomes displayed restarts its
   animations from the top. That is what makes this replay on every tab change
   without a line of JavaScript.                                              */
@media (prefers-reduced-motion: no-preference) {
	.st-vtabs .st-tabs__panel > figure,
	.st-tabs--pill .st-tabs__panel > figure {
		animation: st-panel-rise 0.52s cubic-bezier( 0.2, 0.7, 0.2, 1 ) both;
	}
	.st-vtabs .st-tabs__panel > p,
	.st-tabs--pill .st-tabs__panel > p {
		animation: st-panel-rise 0.52s cubic-bezier( 0.2, 0.7, 0.2, 1 ) 0.06s both;
	}
}
@keyframes st-panel-rise {
	from {
		opacity: 0;
		transform: translateY( 14px );
	}
	to {
		opacity: 1;
		transform: none;
	}
}

/* "We also want this slight tonal shift that makes a border that is a bubble."
   Corrected 2026-08-18: the bubble goes round the WHOLE section, not round the
   picture. In the reference the heading, the copy, the list and the media all
   sit inside one rounded tinted container; wrapping only the media made it a
   frame on a photo, which is a different thing.

   Tint and hairline are mixed from palette tokens rather than stated as
   colours, so a reskin carries them. The radius is large enough to read as a
   panel rather than as a rounded rectangle.                                  */
.st-bubble {
	padding: clamp( 1.75rem, 4vw, 3.25rem );
	border-radius: 28px;
	background: color-mix( in srgb, var( --wp--preset--color--ti-fg-alt ) 24%, transparent );
	border: 1px solid color-mix( in srgb, var( --wp--preset--color--ti-fg ) 9%, transparent );
}
/* The picture keeps a radius so it sits with the container, but no tint and no
   border of its own: two nested frames read as a mistake.                    */
.st-vtabs .st-tabs__panel > figure,
.st-tabs--pill .st-tabs__panel > figure {
	margin: 0;
}
.st-vtabs .st-tabs__panel > figure img,
.st-tabs--pill .st-tabs__panel > figure img {
	border-radius: 14px;
}

/* ── Three abreast (2026-08-18) ─────────────────────────────────────────────
   "Those three sections, instead of being on horizontal each in their own
   section, will be in a row of three like this, using the blue."
   Each card is the same bubble as the section treatment above, so the page has
   one rounded-panel idea rather than two that nearly match. Equal height comes
   from the grid, and the copy is pushed apart so cards of different lengths
   still line their pictures and headings up.                                 */
.st-trio__head {
	margin-bottom: clamp( 2rem, 4vw, 3.25rem );
}
.st-trio {
	display: grid;
	gap: 1.5rem;
	grid-template-columns: 1fr;
}
@media (min-width: 782px) {
	.st-trio {
		grid-template-columns: repeat( 3, minmax( 0, 1fr ) );
		gap: clamp( 1.25rem, 2.5vw, 2rem );
	}
}
.st-trio__card {
	display: flex;
	flex-direction: column;
	gap: 1rem;
	padding: clamp( 1.5rem, 2.4vw, 2rem );
	border-radius: 24px;
	background: color-mix( in srgb, var( --wp--preset--color--ti-fg-alt ) 24%, transparent );
	border: 1px solid color-mix( in srgb, var( --wp--preset--color--ti-fg ) 9%, transparent );
}
.st-trio__card > figure {
	margin: 0;
}
.st-trio__card img,
.st-trio__card svg {
	display: block;
	width: 100%;
	height: auto;
	border-radius: 14px;
}
.st-trio__card img {
	aspect-ratio: 4 / 3;
	object-fit: cover;
}
/* The graphic is drawn, not photographed, so it gets the same box as the
   photographs rather than sitting at whatever height its viewBox implies.   */
.st-trio__card .st-ethereal {
	aspect-ratio: 4 / 3;
}

/* "Add the blue dots in this area and try and have it be less in the areas
   where there is text to keep the text legible."

   ☠️ The obvious build puts the field behind the hero like every other section
   and it does not work here. The hero loop is an OPAQUE WebP on `ti-bg` — that
   is what took it from 1011 KB to 420 KB, and a transparent re-render at the
   same smoothness costs the whole megabyte back. Behind an opaque image the
   field simply stops at its rectangle, and a hard box of missing dots is worse
   than no dots at all.

   So the canvas goes OVER the image instead of under it, and the image is left
   out of the copy wrapper. The image's own ground is `ti-bg`, the same paper as
   the section, so dots crossing onto it are seamless. Two masks intersected
   keep it honest: a hole over the mark so the ghost stays clean, and a ramp
   that holds the field back over the text column.                            */
/* "The ghost animation should be in front of the blue dots."
   It was the other way round: the canvas was raised over the image so the field
   could cross onto it without stopping at its rectangle. Dropping that puts the
   ghost in front, and the rectangle stays invisible only because the field is
   masked away across the whole area the image occupies. The ellipse has to
   COVER the image, not just its middle, or the cut comes back at the corners. */
.tg-halftone:has( .st-split--hero ) .tg-halftone__canvas {
	-webkit-mask-image:
		radial-gradient( 42% 78% at 72% 50%, transparent 0 72%, #000 100% ),
		linear-gradient( to right, rgba( 0, 0, 0, 0.14 ) 0 34%, rgba( 0, 0, 0, 0.45 ) 48%, #000 62% );
	mask-image:
		radial-gradient( 42% 78% at 72% 50%, transparent 0 72%, #000 100% ),
		linear-gradient( to right, rgba( 0, 0, 0, 0.14 ) 0 34%, rgba( 0, 0, 0, 0.45 ) 48%, #000 62% );
	-webkit-mask-composite: source-in;
	mask-composite: intersect;
}
/* The copy still has to clear the raised canvas. */
.tg-halftone:has( .st-split--hero ) .tg-halftone__copy {
	position: relative;
	z-index: 2;
}

/* "Would it be possible to put like a soft white glow underneath the text so it
   is slightly lighter and easier to read?"

   A glow on the glyphs rather than a panel behind the block. A rectangle of
   lighter ground would announce itself as a box on a section whose whole point
   is an even field; a text-shadow hugs the letterforms, so the dots keep
   running right up to the type and the type still wins.

   Three stacked shadows: a tight one that does the actual contrast work and two
   wide faint ones that keep the edge from looking like a stroke. White rather
   than the paper token on purpose — she asked for slightly LIGHTER than the
   ground, and `ti-bg` is the ground.

   Scoped to headings and paragraphs. `p` cannot catch the buttons, which are an
   <a> inside `.wp-block-button`, and a glowing pill would look broken.        */
.tg-halftone__copy :is( h1, h2, h3, p ) {
	text-shadow:
		0 0 8px #fff,
		0 0 18px #fff,
		0 0 34px #fff,
		0 0 58px rgba( 255, 255, 255, 0.95 ),
		0 0 92px rgba( 255, 255, 255, 0.7 );
}

/* ── A real backdrop, because more shadows do not get there ─────────────────
   Stacking text-shadow layers stops paying off quickly: each one is a Gaussian
   falloff of the GLYPH, so more of them deepen the halo right at the letter
   edges and cannot spread into a field. Duplicating the list looks almost
   identical to not duplicating it.

   So the lift comes from a blurred white plate behind each block of copy, and
   the shadows above stay only to keep the letter edges crisp against it. The
   plate is a solid, so its strength is a real number rather than the tail of a
   curve; `--tg-glow` is that number and a section can turn it down.

   `z-index: -1` keeps it behind the text but still inside the copy wrapper's
   stacking context, so it never goes behind the canvas.                      */
.tg-halftone__copy :is( h1, h2, h3, p ) {
	position: relative;
	--tg-glow: 0.9;
}
.tg-halftone__copy :is( h1, h2, h3, p )::before {
	content: "";
	position: absolute;
	inset: -0.55em -1.15em;
	z-index: -1;
	border-radius: 999px;
	background: #fff;
	opacity: var( --tg-glow );
	filter: blur( 22px );
	pointer-events: none;
}
/* The eyebrow is one short word; a plate sized off it reads as a lozenge. */
.tg-halftone__copy p.has-space-mono-font-family::before {
	inset: -0.4em -0.8em;
	filter: blur( 14px );
}

/* ⚠️ It is NOT that the rule misses the tab panels — measured, the panel
   paragraph carries the same shadow as the h1. The field is MASKED, so it is
   faint behind the heading and the lead at the top of the section and at full
   strength by the time it reaches the panel. The first glow was tuned against
   the faint end and vanished at the dense end. Three fully opaque layers now
   do the knock-back and two wide translucent ones feather it, so it holds up
   where the dots are heaviest.                                               */
@supports not ( selector( :is( h1 ) ) ) {
	.tg-halftone__copy h1,
	.tg-halftone__copy h2,
	.tg-halftone__copy h3,
	.tg-halftone__copy p {
		text-shadow: 0 0 8px #fff, 0 0 18px #fff, 0 0 34px #fff;
	}
}

/* ═══ Corporate merch, dev-instruction pass (2026-08-21) ═════════════════════
   Nine numbered tasks plus a global block. Everything here is scoped to classes
   this page carries, so no other page moves.

   ⚠️ The spec's three colours are given as hexes: #111111 ink, #FAFAFA bone,
   #EDEDED panel. Two of them already exist as tokens at very slightly different
   values (ti-fg #0a0a0a, ti-bg #fafaf7) and this theme reskins by token, so the
   tokens are used for those. The panel grey is not a palette slot, so rather
   than stating it here it is a theme.json custom token
   (settings.custom.stPanel -> --wp--custom--st-panel), which keeps it
   reskinnable per store. The hex below is only the no-global-styles fallback
   for Storybook and the block preview kit.                                    */
:root {
	--st-panel: var( --wp--custom--st-panel, #ededed );
}

/* GLOBAL · section rhythm. 96 desktop, 56 mobile. The sections carry inline
   padding from their block attributes, which no stylesheet can beat, so the
   content pass strips that and this owns it instead.                         */
/* The sections carry their padding as a block attribute, which lands as an
   inline style, and no stylesheet beats an inline style without !important.
   The content pass normalises every section to 6rem (96px) inline, so desktop
   needs nothing here and only the mobile step has to fight for it.          */
@media (max-width: 781px) {
	.st-sec {
		padding-top: 56px !important;
		padding-bottom: 56px !important;
	}
}

/* GLOBAL · the eyebrow every section now carries. */
.st-eyebrow {
	font-family: var( --wp--preset--font-family--space-mono );
	font-size: 13px;
	letter-spacing: 1.5px;
	text-transform: uppercase;
	color: var( --wp--preset--color--ti-fg );
	margin: 0 0 0.9rem;
}

/* GLOBAL · alternating rows: text column capped so lines stay readable. */
.st-split__body {
	max-width: 480px;
}

/* TASK 2 · trust strip. One centred line, wrapping cleanly rather than
   scrolling, because five items will not fit a phone on one line.           */
.st-trust {
	min-height: 56px;
	display: flex;
	align-items: center;
	justify-content: center;
	padding-block: 0.75rem;
}
.st-trust p {
	margin: 0;
	font-family: var( --wp--preset--font-family--space-mono );
	font-size: 13px;
	letter-spacing: 2px;
	text-transform: uppercase;
	text-align: center;
	color: var( --wp--preset--color--ti-fg );
	max-width: 62rem;
}

/* TASK 5 · the three cards become solid text panels.
   The body pins to the bottom through a flex spacer, so three headlines of
   different lengths still leave all three bodies sharing one top edge. That is
   the part a plain grid cannot do: equal HEIGHT is free, equal BASELINE is not. */
.st-trio__card {
	background: var( --st-panel );
	border: 0;
	border-radius: 32px;
	padding: 32px;
	gap: 0;
}
.st-trio__card .st-tag {
	display: inline-block;
	align-self: flex-start;
	font-family: var( --wp--preset--font-family--space-mono );
	font-size: 12px;
	letter-spacing: 1px;
	text-transform: uppercase;
	color: var( --wp--preset--color--ti-fg );
	border: 1px solid var( --wp--preset--color--ti-fg );
	border-radius: 8px;
	padding: 4px 10px;
	margin: 0 0 1.25rem;
}
.st-trio__card h3 {
	margin: 0;
}
.st-trio__card .st-spacer {
	flex: 1 1 auto;
	min-height: 1.5rem;
}
.st-trio__card p:not( .st-tag ) {
	margin: 0 0 1.25rem;
}
.st-trio__card .st-cta-link {
	margin: 0;
	font-weight: 700;
}
.st-trio__card .st-cta-link a {
	color: var( --wp--preset--color--ti-fg );
	text-underline-offset: 3px;
}
/* The arrow is drawn here, not typed into the copy.
   ⚠️ WordPress staticizes emoji in post content, and U+2197 is in its emoji
   set, so a typed arrow came out as <img class="emoji"> loaded from s.w.org.
   That put an image inside every card, which the spec forbids, and pulled a
   third-party request on a page that otherwise makes none. CSS content is not
   staticized, so this renders as text.                                       */
.st-trio__card .st-cta-link a::after {
	content: " ↗";
	font-variant-emoji: text;
}

/* TASK 6 · quiet the retail chrome in the reorder carousel.
   ⚠️ Suppression only. The card itself is `content-product.php` in the
   commerce layer, which this side does not own, so the price and the count
   badge are hidden here rather than removed at the source. A real change
   belongs to whoever owns that template.                                    */
.st-bestsellers .price,
.st-bestsellers .woocommerce-Price-amount,
.st-bestsellers .dtla_product__price {
	display: none !important;
}
.st-bestsellers .dtla_product__count,
.st-bestsellers .product-count-badge,
.st-bestsellers [class*="count-badge"] {
	display: none !important;
}

/* TASK 7 · how it works. Equal columns on desktop, stacked on a phone. */
.st-steps {
	display: grid;
	gap: 2rem;
	grid-template-columns: 1fr;
}
@media (min-width: 782px) {
	.st-steps {
		grid-template-columns: repeat( 3, minmax( 0, 1fr ) );
		gap: clamp( 2rem, 4vw, 3.5rem );
	}
}
.st-steps__n {
	font-family: var( --wp--preset--font-family--space-mono );
	font-size: 13px;
	letter-spacing: 2px;
	color: var( --wp--preset--color--ti-fg );
	margin: 0 0 0.75rem;
}
.st-steps h3 {
	margin: 0 0 0.5rem;
}
.st-steps p:not( .st-steps__n ) {
	margin: 0;
}

/* TASK 8 · the closing CTA is two buttons, filled and outline. */
.st-endcta .wp-block-buttons {
	gap: 0.75rem;
}
.st-endcta .is-style-outline .wp-block-button__link {
	background: transparent;
	color: var( --wp--preset--color--ti-fg );
	border: 1px solid var( --wp--preset--color--ti-fg );
}

/* ── Checkout & cart trust badges · give the symbol its own contrast ────────
 *
 * The three badges under Place Order (form-checkout.php) and in the cart
 * totals (cart-totals.php) are icomoon icon-FONT glyphs, not SVG: each is
 * 6-8 stacked .pathN spans and every layer's colour is baked into the
 * generated src/scss/icons.scss.
 *
 * That export ships a "silver medal" ramp - badge #E0DCD2 / #D4D0C4, symbol
 * #FFFFFF / #F1EFE9 - so the badge reads at 11.6:1 against the dark checkout
 * while the symbol INSIDE it reads at 1.35:1 against the badge (and 1.09:1
 * between its two tones). The shield's lock, the star and the clock hands are
 * therefore invisible at 48px and the set looks like three pale blobs.
 *
 * ⚠️ It is not a badge-vs-background problem. Brightening the icon, or
 * recolouring it to a lighter token, changes nothing - the symbol tracks the
 * badge. The separation has to be between the two.
 *
 * Fixed here rather than in icons.scss deliberately:
 *   - icons.scss is an icomoon export; a re-export from icomoon.io reverts it.
 *   - it compiles to build/css/icons-style.css, which is gitignored and
 *     enqueued with STYLES_VERSION - the mtime of main-style.scoped.css, NOT
 *     of icons-style.css (functions.php:423). Shipping a rebuilt icons sheet
 *     on its own leaves the URL byte-identical and every warm cache keeps the
 *     old colours, which reads as "the deploy did nothing".
 *   style.css is versioned by its own hand-bumped Version: header, so this
 *   busts correctly with no vite build and no build/ deploy.
 *
 * ⚠️ The doubled class is load-bearing. icons.scss scores (0,2,1), and
 * style-icons is enqueued from the legacy block (functions.php:672) with no
 * ordering guarantee against the child stylesheet - so an equal-specificity
 * rule here would be a coin toss decided by sheet order. (0,3,1) settles it
 * whichever way they land.
 *
 * Only `color` is restated. The `content` codepoints and the negative
 * margin-left that stacks the layers still come from icons.scss.
 */
.icon-checkout-secure-icons,
.icon-productBadgesIcon3,
.icon-productBadgesIcon2 {
	--st-badge-face: var( --elements-secondary-lighter-1, #e0dcd2 );
	--st-badge-shade: var( --elements-secondary, #cac6b9 );
	--st-badge-mark: var( --bg-primary, #24272a );
	--st-badge-mark-2: var( --dark-blue-500, #323b4e );
}

/* The badge body. */
.icon-checkout-secure-icons.icon-checkout-secure-icons .path1:before,
.icon-checkout-secure-icons.icon-checkout-secure-icons .path5:before,
.icon-productBadgesIcon3.icon-productBadgesIcon3 .path1:before,
.icon-productBadgesIcon3.icon-productBadgesIcon3 .path5:before,
.icon-productBadgesIcon2.icon-productBadgesIcon2 .path1:before,
.icon-productBadgesIcon2.icon-productBadgesIcon2 .path5:before {
	color: var( --st-badge-face );
}

/* The badge's shaded face, which gives it its form. */
.icon-checkout-secure-icons.icon-checkout-secure-icons .path2:before,
.icon-checkout-secure-icons.icon-checkout-secure-icons .path6:before,
.icon-productBadgesIcon3.icon-productBadgesIcon3 .path2:before,
.icon-productBadgesIcon3.icon-productBadgesIcon3 .path6:before,
.icon-productBadgesIcon2.icon-productBadgesIcon2 .path2:before,
.icon-productBadgesIcon2.icon-productBadgesIcon2 .path6:before {
	color: var( --st-badge-shade );
}

/* The symbol - lock, star, clock - knocked out of the badge. */
.icon-checkout-secure-icons.icon-checkout-secure-icons .path3:before,
.icon-checkout-secure-icons.icon-checkout-secure-icons .path7:before,
.icon-productBadgesIcon3.icon-productBadgesIcon3 .path3:before,
.icon-productBadgesIcon2.icon-productBadgesIcon2 .path3:before {
	color: var( --st-badge-mark );
}

/* The symbol's second tone. Kept one step lighter than the symbol rather than
 * merged into it, or the clock's hands and the star's facets disappear into
 * their own face - which is the same failure this rule exists to fix, one
 * layer further in. */
.icon-checkout-secure-icons.icon-checkout-secure-icons .path4:before,
.icon-checkout-secure-icons.icon-checkout-secure-icons .path8:before,
.icon-productBadgesIcon3.icon-productBadgesIcon3 .path4:before,
.icon-productBadgesIcon2.icon-productBadgesIcon2 .path4:before {
	color: var( --st-badge-mark-2 );
}

/* ==========================================================================
 * Bulk price-breaks popup table
 * react-modules/src/components/popups/BulkPricingDiscount/BulkPricingDiscountTable.tsx
 * ==========================================================================
 *
 * Ported from hello-elementor-child's version of the same popup: an ink header
 * bar, white rows on hairline dividers, and a sticky method column so the
 * label stays in view while the quantity breaks scroll sideways.
 *
 * Here rather than as Tailwind classes in the component because popups.js
 * ships no CSS - it relies on the theme's compiled sheet, and the utilities
 * the legacy table uses (border-r-border-secondary, px-[10px], ...) are not in
 * it. Classes added to the .tsx would render nothing until build/css was
 * rebuilt and redeployed; style.css busts on its own Version: bump.
 *
 * Colours go through the legacy tokens so the header matches the popup's own
 * Close button (btn--primary is --bg-primary), falling back to theme.json
 * presets where the legacy sheet is absent.
 *
 * The doubled class takes these to (0,2,1)+, clear of any generic table rule.
 */
.st-bulk-table.st-bulk-table {
	--st-bulk-head: var( --bg-primary, var( --wp--preset--color--ti-bg-inv, #0a0a0a ) );
	--st-bulk-head-text: var( --white-1000, #fff );
	--st-bulk-cell: var( --white-1000, #fff );
	--st-bulk-line: var( --border-secondary, var( --wp--preset--color--ti-line, #e7e4db ) );
	--st-bulk-radius: var( --border-radius-4xs, 8px );

	border-radius: var( --st-bulk-radius );
	overflow: hidden;
}

.st-bulk-table.st-bulk-table .st-bulk-table__scroll {
	width: 100%;
	overflow-x: auto;
	overflow-y: hidden;
}

.st-bulk-table.st-bulk-table .st-bulk-table__scroll::-webkit-scrollbar {
	height: 6px;
}

.st-bulk-table.st-bulk-table table {
	width: 100%;
	margin: 0;
	border: 0;
	border-collapse: separate;
	border-spacing: 0;
	white-space: nowrap;
	text-align: left;
	font-variant-numeric: tabular-nums;
}

.st-bulk-table.st-bulk-table th,
.st-bulk-table.st-bulk-table td {
	padding: 8px 10px;
	border: 0;
	text-align: left;
	vertical-align: middle;
}

.st-bulk-table.st-bulk-table th:first-child,
.st-bulk-table.st-bulk-table td:first-child {
	padding: 8.5px 16px 8.5px 12px;
}

/* Header bar. */
.st-bulk-table.st-bulk-table thead th {
	height: 40px;
	background: var( --st-bulk-head );
	color: var( --st-bulk-head-text );
	font-size: 14px;
	font-weight: 500;
	line-height: 1.3;
}

/* Body rows. */
.st-bulk-table.st-bulk-table tbody th,
.st-bulk-table.st-bulk-table tbody td {
	background: var( --st-bulk-cell );
	font-size: 15px;
	font-weight: 400;
	line-height: 1.4;
	border-bottom: 1px solid var( --st-bulk-line );
	border-right: 1px solid var( --st-bulk-line );
}

.st-bulk-table.st-bulk-table tbody tr > :first-child {
	border-left: 1px solid var( --st-bulk-line );
}

.st-bulk-table.st-bulk-table tbody tr:last-child > :first-child {
	border-bottom-left-radius: var( --st-bulk-radius );
}

.st-bulk-table.st-bulk-table tbody tr:last-child > :last-child {
	border-bottom-right-radius: var( --st-bulk-radius );
}

/* The method column stays put while the breaks scroll under it. */
.st-bulk-table.st-bulk-table th:first-child {
	position: sticky;
	left: 0;
	z-index: 1;
}

.st-bulk-table.st-bulk-table thead th:first-child {
	z-index: 2;
}


/* ── Shop Titan · Filter Everything sidebar (2026-09-21) ─────────────────────
   The `[fe_widget]` shortcode in inc/woocommerce/archive.php:108 rendered with
   ZERO theme CSS until this section existed — grep style.css for `wpc-` before
   this commit and you get nothing. What shipped on every store was the
   plugin's own defaults: native OS checkboxes, `#8a8a8a` hairlines, no panel,
   no rhythm, on a 210px rail. It read as an unstyled plugin because it was one.

   ⚠️ NOT ONE HEX IN THIS SECTION, AND THAT IS THE WHOLE DESIGN.

   Every colour resolves to one of the eight `ti-*` palette slugs. Those slugs
   are a validated contract, not a convention: shoptitan-core's `TI_SLUGS`
   (tools/stores.py:64) requires all eight, and `StoreSpec.problems()`
   (tools/stores.py:352) rejects a spec that sets a partial palette or one that
   still equals `THREADGHOST_PALETTE`. Each store writes its own eight to
   `wp_global_styles`. So this one file is already a different brand on each
   store, with no per-store rule anywhere:

     ti-accent   safetyuniformco #ff6a00 · chefsoutfitters #8a1f1f · polosupplyco #123d2e
     ti-line     #d7dcdf cool grey       · #d8d2ca warm           · #d2d7d5 green-grey
     ti-bg-alt   #f2f4f4                 · #f6f4f1               · #f3f5f4

   The hairlines and panel fills carry brand temperature on their own, so the
   rail reads cool-industrial on Safety Uniform and warm on Chefs Outfitters
   before the orange or the red appears anywhere. Do NOT add a per-store filter
   block to stores/<slug>/brand.css — if a store needs to differ, it differs by
   its palette, which it already sets.

   No fallbacks on the var()s either. theme.json defines all eight slugs, so a
   fallback here is unreachable code that would only ever mask a broken
   global-styles post by quietly painting ThreadGhost's grey.

   ⚠️ FONTS ARE NOT SHAREABLE THE WAY COLOURS ARE. Colours share eight slugs;
   font families do not — each store registers its OWN preset slugs (`su-head`,
   `co-head`, `po-head`, from `Fonts.head_slug`, tools/stores.py:196). Writing
   `var(--wp--preset--font-family--su-head)` here would style safetyuniformco
   and silently no-op into the browser default on the other fourteen. So type
   is `inherit` throughout: `global_styles_json()` sets the body family on root,
   which is per-store correct for free. The display face is deliberately not
   used — there is no CSS-reachable variable holding it, and small UI labels in
   a narrow rail read better as tracked-out caps in the body face anyway.

   Specificity: the plugin's own rules are compounded (`body
   .wpc-filters-main-wrap li.wpc-term-item input[type=checkbox]` is 0,3,3), so
   these use the `body#dtla_body_id.dtla__body` prefix the archive overrides
   above already use. That is 1,4,3 and wins without a single `!important`.

   Verified against the live DOM at https://safetyuniformco.com/products/ and
   against filter-everything.min.css 1.9.2 — every class name below was read
   off the page, not guessed. */

/* ── Shop Titan · TYPE SCALE line-height (2026-09-30) ────────────────────────
   theme.json's `elements.heading.lineHeight` is ONE value for every heading
   level (1.1, FLOOR.md (g)'s h1). h2 (1.15) and h3 (1.3) are the two levels
   that differ from it. Sizes are theme.json `typography.fontSizes`. */
h2 {
	line-height: 1.15;
}

h3 {
	line-height: 1.3;
}

/* ── Shop Titan · FLEET SHAPE TOKENS (2026-09-21) ────────────────────────────
   The third token axis. The `ti-*` palette carries colour and the fontFamilies
   carry type; until this existed nothing carried SHAPE, so every store in the
   roll-up shared one silhouette however different its brand — which is exactly
   what `docs/CONTENT-AND-DESIGN.md`'s 2026-09-17 diagnosis named ("no shape or
   rhythm tokens").

   Defaults live in theme.json `settings.custom`; a store overrides them in its
   own `wp_global_styles` post, the same place its palette and fonts live, and
   never in this repo. EVERY FALLBACK BELOW IS TODAY'S RENDERED VALUE, so a
   store that sets nothing is pixel-identical to before this section existed.

   ⚠️ THIS ALIAS BLOCK IS THE POINT — do not read `--wp--custom--st-*` directly
   anywhere else. A design-system page is coming that Phase 3 must adopt and it
   may well name these differently; re-pointing six declarations here is a
   one-line-each change, whereas a rename reaching into every consuming rule is
   a sweep through the whole stylesheet.

   ⚠️ RADIUS IS TWO STEPS, NOT ONE. Panels are 12px today (set on
   `.dtla-shop-archive__filters` by woocommerce-style.css) while controls are
   2px. Collapsing both onto one token would have silently reshaped every
   store's panel the moment this shipped, which is precisely the "defaults
   reproduce today" guarantee this section is built on. Keep them separate.

   ⚠️ `--st-density` is a MULTIPLIER, not a length. It is consumed inside
   `calc()` against each component's own base spacing, so a profile stays
   correct on a component whose rhythm is not the filter rail's. A raw length
   here would flatten every component to the same spacing. */
:root {
	--st-radius: var( --wp--custom--st-radius, 2px );
	--st-radius-lg: var( --wp--custom--st-radius-lg, 12px );
	--st-rule: var( --wp--custom--st-rule, 2px );
	--st-density: var( --wp--custom--st-density, 1 );
	--st-case: var( --wp--custom--st-case, uppercase );
	--st-track: var( --wp--custom--st-track, 0.12em );
}

/* The local alias layer. One place to retune the rail; each value still
   resolves to a palette slug, so retuning never reintroduces a literal.

   Declared on the chip wrapper as well as the rail. The plugin renders the
   active-filter chips itself, inside `<header class="woocommerce-products-
   header">` under the H1 — `.wpc-custom-selected-terms`, which is nowhere near
   the sidebar in the DOM, so it inherits none of these unless named here. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters,
body#dtla_body_id.dtla__body .wpc-custom-selected-terms {
	--st-filter-ink: var( --wp--preset--color--ti-fg );
	--st-filter-meta: var( --wp--preset--color--ti-accent-secondary );
	--st-filter-line: var( --wp--preset--color--ti-line );
	--st-filter-surface: var( --wp--preset--color--ti-bg-alt );

	/* The filled-control pair, lifted verbatim from theme.json's
	   `elements.button` (background ti-accent / text ti-fg-alt). A checked
	   checkbox IS a filled control, so reusing the button's pair means the
	   contrast was already checked per store by whoever set the palette —
	   rather than this section inventing a second, unchecked pairing. */
	--st-filter-active: var( --wp--preset--color--ti-accent );
	--st-filter-on-active: var( --wp--preset--color--ti-fg-alt );

	font-size: 0.875rem;
	line-height: 1.45;
}

/* ── The rail itself ──────────────────────────────────────────────────────
   Sticky because the category list runs 32 terms on safetyuniformco — long
   enough that scrolling to row four of the grid leaves the filters off-screen
   entirely. `top` clears the sticky header; max-height + auto keeps the rail
   from growing taller than the viewport and trapping the last few terms. */
@media (min-width: 1024px) {
	body#dtla_body_id.dtla__body .dtla-shop-archive__sidebar {
		position: sticky;
		/* Under the sticky header: header.js publishes the measured height. */
		top: calc(var(--header-height, 7rem) + 1.5rem);
		align-self: start;
		max-height: calc(100vh - var(--header-height, 7rem) - 3rem);
		overflow-y: auto;
		overscroll-behavior: contain;
		/* Scrollbar in brand, not the browser's default blue-grey. */
		scrollbar-width: thin;
		scrollbar-color: var( --wp--preset--color--ti-line ) transparent;
	}
}

/* ── One panel, not two ───────────────────────────────────────────────────
   ⚠️ `.dtla-shop-archive__filters` IS ALREADY A PANEL. `woocommerce-style.css`
   gives it `1px solid #e5e7eb`, `border-radius:12px`, white, 16px padding. An
   earlier pass here added a second bordered box on `.wpc-filters-main-wrap`
   inside it — a visible double border, and more importantly 72px of the 240px
   rail spent on nested chrome (16+1 outside, 18+1 inside, per side). Measured:
   the term `<ul>` came out 168px wide, which is what was still wrapping the
   long labels after the width went to 240.

   So: retint the panel that exists, and let the plugin's wrapper contribute
   nothing. That hands 38px straight back to the labels.

   The radius is deliberately left alone. 12px is the donor's and it does sit
   oddly next to square product cards and safetyuniformco's square buttons —
   but radius is shape, not colour, and the palette does not carry it. Changing
   it is a design call for the store, not something this section should smuggle
   in. Flagged, not fixed. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters {
	border-color: var( --st-filter-line );
	background: var( --wp--preset--color--ti-bg );
	border-radius: var( --st-radius-lg );
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-filters-main-wrap {
	border: 0;
	padding: 0;
	background: none;
}

/* ── Filter group headings ────────────────────────────────────────────────
   The plugin ships `.wpc-filter-title` as a bare div at inherited size, which
   is why "Categories" and "Brands" read as list items rather than headings.
   Tracked-out caps at a SMALLER size is the standard small-label treatment on
   this theme — same shape as the "About this catalog" eyebrow in
   templates/archive-product.html — and the rule under it is what separates two
   groups that otherwise run together. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-filter-title {
	font-family: inherit;
	font-size: 0.6875rem;
	font-weight: 700;
	letter-spacing: var( --st-track );
	text-transform: var( --st-case );
	color: var( --st-filter-ink );
	margin: 0 0 calc( 0.875rem * var( --st-density ) );
	padding-bottom: 0.625rem;

	/* ⚠️ ACCENT, NOT ti-line, AND THAT IS THE POINT (2026-09-21).
	   The first version of this section used the accent ONLY on a checked box,
	   which meant that on the page a visitor actually lands on — nothing checked
	   — the rail carried no brand colour at all and safetyuniformco, chefs-
	   outfitters and polosupplyco were pixel-identical greys. The palette was
	   working; it just was not visible at rest.

	   A rule under each group heading is a CONTROLLED dose: two lines per rail,
	   same weight whatever the hue. Compare the two treatments that were tried
	   and rejected, both token-only and both worse:

	   · accent on the +/− toggles — amplifies loud palettes and mutes quiet
	     ones. safetyuniformco's orange `+` shouted; polosupplyco's #123d2e read
	     as plain black. The same rule landing differently per store is not
	     distinction, it is inconsistency.
	   · an inverted `ti-bg-inv` heading bar — bolder, and differentiates LESS:
	     that slug is #141a22 / #1a1613 / #101418 across the three stores, so the
	     bar looks the same everywhere. Weight is not difference. */
	border-bottom: var( --st-rule ) solid var( --st-filter-active );
}

/* Plugin default is 32px, which on a hairlined panel reads as a gap rather
   than as a break between two groups. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-filters-section {
	margin-bottom: calc( 1.75rem * var( --st-density ) );
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-filters-section:last-child {
	margin-bottom: 0.75rem;
}

/* ── Term rows ────────────────────────────────────────────────────────────
   The plugin lays the row out with flex and `align-items:center`. Top-align
   instead: "FR / Flame-Resistant" and "Hi-Vis Hoodies" wrap to two lines in
   this rail, and centred alignment floats the checkbox against the middle of a
   two-line label instead of against its first line. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-term-item-content-wrapper {
	align-items: flex-start;
	padding-top: calc( 0.3125rem * var( --st-density ) );
	padding-bottom: calc( 0.3125rem * var( --st-density ) );
	min-height: 0;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters ul.wpc-filters-ul-list {
	margin: 0;
	padding: 0;
	list-style: none;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item {
	margin: 0;
	list-style: none;
}

/* ── The checkbox ─────────────────────────────────────────────────────────
   The single biggest reason the screenshot reads as unstyled: these are the
   browser's native controls. `appearance:none` plus a drawn checkmark is what
   makes them the store's.

   The mark is drawn as a rotated border-corner rather than a glyph so it needs
   no font and no icon file — which matters because the check has to sit on
   ti-accent, and ti-accent is near-black on ThreadGhost and safety orange on
   safetyuniformco. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item input[type="checkbox"] {
	appearance: none;
	-webkit-appearance: none;
	flex: 0 0 auto;
	width: 1.0625rem;
	height: 1.0625rem;
	margin: 0.0625rem 0.625rem 0 0;
	padding: 0;
	position: relative;
	background: var( --wp--preset--color--ti-bg );
	border: 1px solid var( --st-filter-line );
	border-radius: var( --st-radius );
	cursor: pointer;
	transition: background-color 0.16s ease, border-color 0.16s ease;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item input[type="checkbox"]:hover {
	border-color: var( --st-filter-active );
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item input[type="checkbox"]:checked {
	background: var( --st-filter-active );
	border-color: var( --st-filter-active );
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item input[type="checkbox"]:checked::after {
	content: "";
	position: absolute;
	left: 0.3125rem;
	top: 0.0625rem;
	width: 0.25rem;
	height: 0.5625rem;
	border: solid var( --st-filter-on-active );
	border-width: 0 2px 2px 0;
	transform: rotate(42deg);
}

/* Keyboard focus. The plugin sets `outline:0` on these inputs, which removes
   the only focus affordance a keyboard user has — 32 checkboxes with no
   visible focus ring is a real accessibility defect, not a cosmetic one. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item input[type="checkbox"]:focus-visible {
	outline: 2px solid var( --st-filter-active );
	outline-offset: 2px;
}

/* ── Term labels ──────────────────────────────────────────────────────────
   The label text is an <a class="wpc-filter-link"> inside the <label>. The
   theme's global link colour and underline apply to it, so 32 terms render as
   32 underlined links — and the plugin's click handler preventDefault()s every
   one of them, so the underline promises navigation that never happens.

   The `span[data-wpc-span-link]` half of each pair is for the span-swap in
   hello-elementor-child/inc/integrations/filter-everything/index.php, which
   rewrites these anchors as spans. It is not installed on this theme yet (see
   the note at the end of this section), so the span selectors are inert here —
   they are present so that landing that module needs no CSS change. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item label {
	margin: 0;
	cursor: pointer;
	min-width: 0;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item label a.wpc-filter-link,
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item label span[data-wpc-span-link] {
	font-family: inherit;
	font-size: 0.875rem;
	line-height: 1.35;
	color: var( --st-filter-ink );
	text-decoration: none;
	border: 0;
	cursor: pointer;
	transition: color 0.16s ease;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item label:hover a.wpc-filter-link,
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item label:hover span[data-wpc-span-link] {
	color: var( --st-filter-active );
}

/* A term whose box is ticked should read as ticked even when the eye is on the
   label rather than the box. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item input[type="checkbox"]:checked + label a.wpc-filter-link,
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item input[type="checkbox"]:checked + label span[data-wpc-span-link] {
	color: var( --st-filter-active );
	font-weight: 600;
}

/* ── Reclaim the toggle gutter on rows that have no toggle ───────────────
   The plugin reserves 32px of right padding on EVERY row of a hierarchical
   filter (`.wpc-filter-has-hierarchy .wpc-term-item-content-wrapper`), but only
   a row with children actually renders the `+`. On safetyuniformco that is 5
   rows out of 32 — the other 27 were each donating 32px to an icon that does
   not exist, and it is the single biggest reason long terms wrapped.

   Measured on the live rail at 240px before this rule: wrapper 168px, of which
   the label got 109 and the link 76 — and "FR / Flame-Resistant" needs ~145.
   Giving the gutter back takes the link to ~150 and it fits on one line. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item:not(.wpc-has-children) > .wpc-term-item-content-wrapper {
	padding-right: 0;
}

/* ── The label is a flex container, so the link cannot use the row ────────
   The plugin sets `display:flex` on the <label>, which makes the term link and
   its count siblings competing for width rather than text flowing around a
   count. Without `min-width:0` the link is floored at its longest word and the
   count is pushed out; with baseline alignment the count stops floating at the
   top-right of a two-line label, which was the thing that read as broken. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item label {
	flex: 1 1 auto;
	align-items: baseline;
	column-gap: 0.375rem;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item label a.wpc-filter-link,
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item label span[data-wpc-span-link] {
	min-width: 0;
}

/* ── The selected row ─────────────────────────────────────────────────────
   A tint plus an accent spine, so "what am I filtered by" is answerable from
   the shape of the rail rather than by reading 33 labels for a bold one.

   ⚠️ THE `>` COMBINATORS ARE LOAD-BEARING. A nested term's `ul.children` lives
   INSIDE its parent `<li>`, so the obvious
   `li.wpc-term-item:has(input:checked)` also matches the PARENT — tick
   "Jackets & Outerwear" and "Jackets" lights up too, claiming a filter that is
   not applied. Matching only a checkbox that is a direct child of this row's
   own content wrapper is what scopes it to the row the visitor actually ticked.

   ⚠️ Degrades, does not break. `:has()` is Chrome 105+ / Safari 15.4+ / Firefox
   121+. Where it is missing this whole block is dropped as an unknown selector
   and the selected term still reads — the rule above already gives a checked
   label the accent colour and 600 weight, which is the baseline this enhances
   rather than replaces. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item:has( > .wpc-term-item-content-wrapper > input[type="checkbox"]:checked ) > .wpc-term-item-content-wrapper {
	background: var( --st-filter-surface );
	box-shadow: inset 3px 0 0 var( --st-filter-active );
	margin-left: -0.5rem;
	padding-left: 0.5rem;
}

/* Term counts. Currently suppressed — the widget carries `wpc-show-counts-no`,
   a plugin setting, so `.wpc-term-count` is not emitted. Styled anyway so that
   turning counts on in the plugin UI needs no CSS change; a count is meta,
   which is what ti-accent-secondary is for. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-term-count {
	font-family: inherit;
	font-size: 0.75rem;
	color: var( --st-filter-meta );
	margin-left: 0.375rem;
}

/* ── Nested terms ─────────────────────────────────────────────────────────
   `ul.children` under a parent — Jackets → Jackets & Outerwear. The plugin
   indents with nothing, so a child reads as a sibling. The hairline is the
   cheapest way to show the relationship in a rail this narrow. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-term-item ul.children {
	margin: 0.125rem 0 0.25rem 0.5rem;
	padding-left: 0.5rem;
	border-left: 1px solid var( --st-filter-line );
	list-style: none;
}

/* ── The +/− expander ─────────────────────────────────────────────────────
   `<i class="wpc-toggle-children-list">`, absolutely positioned 24px wide at
   the row's right edge. The plugin draws it as two 12x1px `#8a8a8a` bars —
   `:before` rotated 90deg (the vertical stroke) and `:after` horizontal — and
   hides `:before` on `.wpc-opened`, which is how the + becomes a −. That
   mechanism is fine; only the hard-coded grey is not, so these rules recolour
   the bars and leave the toggle logic entirely to the plugin.

   Top-aligned to match the row: the wrapper is `align-items:flex-start` above,
   and the toggle's own `top:0;bottom:0` would otherwise centre its bars
   against a two-line label. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters i.wpc-toggle-children-list {
	bottom: auto;
	height: 1.875rem;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters i.wpc-toggle-children-list::before,
body#dtla_body_id.dtla__body .dtla-shop-archive__filters i.wpc-toggle-children-list::after {
	background-color: var( --st-filter-meta );
	transition: background-color 0.16s ease;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters i.wpc-toggle-children-list:hover::before,
body#dtla_body_id.dtla__body .dtla-shop-archive__filters i.wpc-toggle-children-list:hover::after {
	background-color: var( --st-filter-active );
}

/* ── Active-filter chips ──────────────────────────────────────────────────
   ⚠️ THE CHIPS ARE ALREADY ON THE PAGE. The plugin renders them itself, in
   `.wpc-custom-selected-terms` inside `<header class="woocommerce-products-
   header">` directly under the H1, on every archive — verified 2026-09-21 on
   /products/, /products/?c=t-shirts and /custom/safety-vests/. They looked
   absent because nothing styled them, not because nothing printed them.

   Do not "add" them with `[fe_chips]`; that prints a second list. The trap is
   that the WIDGET's own chip container (`.wpc-filters-widget-top-container`)
   really is hidden on desktop by the plugin, so the sidebar looks chipless.
   Two different containers — only the in-widget one is hidden, and it is the
   one the mobile drawer uses. Both are styled below. */

/* The plugin prints an empty <ul> on every unfiltered archive and marks it
   `wpc-empty-chips-container`. Left alone that is a flex row with a bottom
   margin sitting under the H1 of every category page — dead space above the
   products on the first screen a visitor sees.

   ⚠️ Both `wpc-filter-chips-list` and `wpc-empty-chips-container` are on the
   same <ul>, so this rule and the `display:flex` one below are a specificity
   TIE and the loser is whichever is written first. Carrying both class names
   here makes it win on specificity instead of on source order — otherwise
   moving either block, which is an innocuous-looking edit, silently brings the
   empty row back. Measured: without the second class this computed to `flex`. */
body#dtla_body_id.dtla__body .wpc-custom-selected-terms ul.wpc-filter-chips-list.wpc-empty-chips-container,
body#dtla_body_id.dtla__body .dtla-shop-archive__filters ul.wpc-filter-chips-list.wpc-empty-chips-container {
	display: none;
}

body#dtla_body_id.dtla__body .wpc-custom-selected-terms {
	margin: 1rem 0 0;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters ul.wpc-filter-chips-list,
body#dtla_body_id.dtla__body .wpc-custom-selected-terms ul.wpc-filter-chips-list {
	margin: 0 0 1.25rem;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.375rem;
	list-style: none;
}

body#dtla_body_id.dtla__body .wpc-custom-selected-terms ul.wpc-filter-chips-list {
	margin-bottom: 0;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-filter-chip a,
body#dtla_body_id.dtla__body .wpc-custom-selected-terms li.wpc-filter-chip a {
	font-family: inherit;
	font-size: 0.75rem;
	line-height: 1.3;
	display: inline-flex;
	align-items: center;
	gap: 0.375rem;
	padding: 0.25rem 0.5rem;
	background: var( --st-filter-surface );
	border: 1px solid var( --st-filter-line );
	border-radius: var( --st-radius );
	color: var( --st-filter-ink );
	text-decoration: none;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-filter-chip a:hover,
body#dtla_body_id.dtla__body .wpc-custom-selected-terms li.wpc-filter-chip a:hover {
	border-color: var( --st-filter-active );
	color: var( --st-filter-active );
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-chip-remove-icon,
body#dtla_body_id.dtla__body .wpc-custom-selected-terms .wpc-chip-remove-icon {
	font-size: 0.875rem;
	line-height: 1;
	color: var( --st-filter-meta );
}

/* "Reset all" carries BOTH `wpc-filter-chip` and `wpc-chip-reset-all`, so it
   matches the chip rules above and has to be un-chipped here. It is an action,
   not a filter you can remove — the plugin already hides its × icon. Must stay
   after the generic chip rules; same specificity, later wins. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-chip-reset-all a,
body#dtla_body_id.dtla__body .wpc-custom-selected-terms li.wpc-chip-reset-all a {
	font-family: inherit;
	font-size: 0.75rem;
	font-weight: 600;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	padding: 0.25rem;
	background: none;
	border: 0;
	color: var( --st-filter-active );
	text-decoration: none;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters li.wpc-chip-reset-all a:hover,
body#dtla_body_id.dtla__body .wpc-custom-selected-terms li.wpc-chip-reset-all a:hover {
	text-decoration: underline;
}

/* ── Term search field ────────────────────────────────────────────────────
   Not emitted on this build (no filter uses the search layout), styled for the
   same reason as the counts: enabling it should not need a CSS change. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters input.wpc-filter-search-field {
	font-family: inherit;
	font-size: 0.8125rem;
	width: 100%;
	padding: 0.5rem 0.625rem;
	background: var( --wp--preset--color--ti-bg );
	border: 1px solid var( --st-filter-line );
	border-radius: var( --st-radius );
	color: var( --st-filter-ink );
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters input.wpc-filter-search-field:focus {
	outline: none;
	border-color: var( --st-filter-active );
}

/* ── Show more / show less ────────────────────────────────────────────────
   32 categories is past the point where a rail should show them all. If a
   filter is switched to the more/less layout these are its controls. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-see-more-control,
body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-see-less-control {
	font-family: inherit;
	font-size: 0.75rem;
	font-weight: 600;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	margin-top: 0.5rem;
	color: var( --st-filter-active );
	cursor: pointer;
}

/* ── Mobile: the open button and the drawer ───────────────────────────────
   The plugin hides `.wpc-open-close-filters-button` by default and reveals it
   at its own breakpoint, so these rules deliberately declare no media query —
   redeclaring that breakpoint here would mean two sources of truth for when
   the rail becomes a drawer. Styling only; visibility stays the plugin's. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-open-close-filters-button {
	font-family: inherit;
	font-size: 0.75rem;
	font-weight: 700;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	padding: 0.75rem 1rem;
	background: var( --st-filter-active );
	color: var( --st-filter-on-active );
	border: 1px solid var( --st-filter-active );
	border-radius: var( --st-radius );
	text-decoration: none;
}

/* The hamburger bars inside that button sit on ti-accent, so they take the
   same on-accent colour as the button's own text, not the plugin grey. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-open-close-filters-button .wpc-icon-html-wrapper span {
	background-color: var( --st-filter-on-active );
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-widget-popup-title {
	font-family: inherit;
	font-size: 0.6875rem;
	font-weight: 700;
	letter-spacing: 0.12em;
	text-transform: uppercase;
	color: var( --st-filter-ink );
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-widget-close-icon .wpc-icon-html-wrapper span {
	background-color: var( --st-filter-ink );
}

/* The drawer's Apply / Cancel pair. Apply is the primary action and takes the
   filled-control pair; Cancel is secondary and takes a hairline. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-filters-apply-button {
	font-family: inherit;
	font-size: 0.75rem;
	font-weight: 700;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	background: var( --st-filter-active );
	color: var( --st-filter-on-active );
	border: 1px solid var( --st-filter-active );
	border-radius: var( --st-radius );
	text-decoration: none;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-filters-close-button {
	font-family: inherit;
	font-size: 0.75rem;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	background: none;
	color: var( --st-filter-meta );
	border: 1px solid var( --st-filter-line );
	border-radius: var( --st-radius );
	text-decoration: none;
}

body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-filters-overlay {
	background: var( --wp--preset--color--ti-bg-inv );
	opacity: 0.55;
}

/* The spinner the plugin shows between a tick and the re-render. */
body#dtla_body_id.dtla__body .dtla-shop-archive__filters .wpc-spinner {
	border-top-color: var( --st-filter-active );
}

/* ⚠️ STYLING DOES NOT CLOSE THE FACET URL HOLE, AND THIS RAIL IS LIVE.
   Every term above is still a real crawlable <a href="?c=...">. The DTLA site
   hit exactly this: a distributed scraper walked the facet space on 2026-09-01
   — 3,112 requests in two minutes, 1,688 unique URLs — saturated php-fpm and
   put the site behind 504s. The fix is already written and proven, as
   hello-elementor-child/inc/integrations/filter-everything/index.php (anchor →
   span swap plus robots.txt disallows); it has never been ported to this
   theme. Making the rail look inviting does not make that safer. Port it. */

/* ═══════════════════════════════════════════════════════════════════════════
   THE FLOOR — 2026-09-25, Michael and Diego with mondaymerch.com beside
   threadghost.com at half-screen width. Rows WB-248 … WB-255. Every value here
   is a theme.json token; tools/floor-check.py proves the CSS says what these
   rows say it should, and the 👁️ intention agent is what proves it LOOKS right.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── The bottom cart bar is retired · WB-250 ─────────────────────────────────
   Michael, V1 18:08: *"I don't like this bottom bar anymore. Let's move it to
   the top."* 19:58: *"The eye just doesn't go below a footer."*

   The bar is `position: fixed; bottom: 0` in the configurator bundle
   (src/components/configurator/productInfoHeader/productInfoHeader.scss), and a
   fixed element's containing block is the viewport, so it has no end — which is
   how it came to sit under the footer. assets/st-product-cta.js MOVES its live
   contents to the top of the product panel and marks the shell; the shell then
   goes, and nothing on a product page is fixed to the bottom of the screen any
   more.

   The bundle is not edited: `src/` is a build input and `build/` is gitignored,
   so a rebuild here would replace 348KB of the live store's CSS with one this
   session compiled. One rule in the theme's own stylesheet reaches every store
   on the next request and reverts nothing.                                    */
.productInfoHeader[data-st-cta-lifted],
body .productInfoHeader[data-st-cta-lifted] {
	display: none !important;
}

/* Belt and braces for the case the lift never runs — a build shipped without the
   legacy CSS, JS blocked. A bar that still has its contents is left alone (a
   visitor keeps an Add to cart), but it is no longer allowed to be fixed, so it
   ends with the page instead of following the scroll past the footer. */
.productInfoHeader:not([data-st-cta-lifted]) {
	position: static !important;
	bottom: auto !important;
}

/* ── The page uses its width · WB-253 ───────────────────────────────────────
   Michael, V1 22:32: *"this could take up a lot more space on that 1040 like you
   were saying."*

   Two things were capping it. The container cap was spelled three ways — 1140px
   inline in every pattern, 1240px in theme.json, 1400px typed into .dtla-header
   (now `--wp--custom--st-shell`). And the legacy layer freezes the container at
   a literal 737px below 1200px:

       --container-max-width: calc(1464px + (var(--container-padding-x) * 2));
       @media (max-width: 1199px) { … calc(737px + …) }     ← src/scss/vars/base/container-width.scss

   At a 1040 window that is a 785px column inside a 1040 viewport: 255px of dead
   space, on the page a buyer actually orders from.

   ⚠️ The VARIABLE is deliberately left alone. `--container-max-width` is what
   every full-bleed element derives its negative margin from
   (src/scss/global/_global.scss:278). Changing it moves every alignfull group on
   the page — the trap this file's archive-width note already records. The cap is
   lifted on the containers themselves instead, so only the column moves.       */
@media (min-width: 900px) and (max-width: 1199.98px) {
	body#dtla_body_id.dtla__body main.container,
	body#dtla_body_id.dtla__body #primary.container,
	body#dtla_body_id.dtla__body .configurator > .container,
	body#dtla_body_id.dtla__body .order__container {
		max-width: calc(100vw - (var(--container-padding-x, 1.5rem) * 2));
	}
}

/* ── Prose has a measure · WB-254 ───────────────────────────────────────────
   Michael, V1 20:15: *"the way that this is written with the line all messed up,
   this is on the right."* 20:41: *"Just keep it simple. Show them what they need.
   Very, very cleanly, a little bit of formatting."*

   One token, in characters rather than pixels, so the measure holds at every
   font size the fluid scale produces. It replaces the 68ch that was typed into
   the category-description rule above.                                        */
.woocommerce-products-header .term-description,
.st-product-cta__wrap .woocommerce-product-details__short-description,
.st-product-cta__wrap .woocommerce-Tabs-panel p,
.entry-content > p,
.st-measure {
	max-width: var(--wp--custom--st-measure, 68ch);
}

/* The footer GRID (`.st-footer__cols`, WB-251 · WB-252) moved to the kit's
   shared layer, assets/st-floor.css (TT-5911 B1). The legal row stays here. */

/* Copyright, Contact, Privacy and Terms. Terms and Privacy get a line of their
   own the moment the row is narrow, instead of being pushed off the right edge
   by `space-between`. */
.st-footer__legal {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 0.75rem 1.5rem;
	/* Clearance for the chat bubble, from the bubble's own token, so the last
	   line of the footer is never underneath it. */
	padding-bottom: calc(
		var(--wp--custom--st-chat-size, 60px) + (var(--wp--custom--st-chat-inset, 1.25rem) * 2)
	);
}

.st-footer__legal__links {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.75rem 1.5rem;
}

@media (max-width: 781.98px) {
	.st-footer__legal {
		flex-direction: column;
		align-items: flex-start;
	}
	.st-footer__legal__links {
		width: 100%;
	}
}

/* The last group inside the footer is that legal row whether or not the part has
   been re-saved with the class, so an existing store gets the clearance too. */
.wp-site-blocks > .wp-block-group:last-child > .wp-block-group:last-child {
	padding-bottom: calc(
		var(--wp--custom--st-chat-size, 60px) + (var(--wp--custom--st-chat-inset, 1.25rem) * 2)
	);
}

/* ── The footer wears the kit's dress · TT-5911 ─────────────────────────────
   Diego, 2026-10-02: "the footer seems to differ from the vercel site." #66
   ported the kit's column GRID; nobody had put the two footers side by side.
   Measured at 1280: the live footer opened on a full-width band (a large
   light-blue site title and a one-line blurb over a rule), sans-serif links in
   `ti-fg-alt`, the legal links on the right; the kit's brand block is the first
   COLUMN, everything is mono, links are the canvas colour, labels are muted,
   and the legal links sit on the left.

   All of it is CSS on the part's existing structure (brand group, rule, the
   `st-footer__cols` columns, rule, the `st-footer__legal` row), so a store whose
   footer part is saved in its own database gets it too, and no link, label or
   column changes: those are the store's menus. The column grid above is
   untouched. `!important` appears where the block's own preset classes
   (`has-ti-fg-alt-color`, `has-small-font-size`) already carry one. */
footer.wp-block-template-part > .wp-block-group {
	--st-footer-ink: var(--wp--preset--color--ti-bg, #fff);
	/* Labels: the footer's own ink at 62%. `ti-accent-secondary` measured too
	   dim on `ti-bg-inv` to read as a label. */
	--st-footer-mute: color-mix(in srgb, var(--st-footer-ink) 62%, transparent);
	--st-footer-mono: var(--wp--preset--font-family--space-mono, ui-monospace, monospace);
	padding-top: 3rem !important;
}
footer.wp-block-template-part > .wp-block-group,
footer.wp-block-template-part > .wp-block-group p,
footer.wp-block-template-part > .wp-block-group li,
footer.wp-block-template-part > .wp-block-group a,
footer.wp-block-template-part > .wp-block-group .wp-block-site-title {
	color: var(--st-footer-ink) !important;
}
footer.wp-block-template-part > .wp-block-group a:hover {
	color: var(--wp--preset--color--ti-line, currentColor) !important;
}
footer.wp-block-template-part .wp-block-site-title,
footer.wp-block-template-part .wp-block-site-title a {
	font-size: 1rem !important;
	font-weight: 800 !important;
	letter-spacing: 0.14em !important;
	text-transform: uppercase !important;
	line-height: 1.2;
}
footer.wp-block-template-part > .wp-block-group > .wp-block-group:first-child p {
	font-family: var(--st-footer-mono);
	font-size: 0.8125rem !important;
	line-height: 1.7 !important;
}
footer.wp-block-template-part > .wp-block-group > .wp-block-group:first-child p.has-ti-accent-secondary-color {
	color: var(--st-footer-mute) !important;
}
footer.wp-block-template-part .st-footer__cols h3 {
	color: var(--st-footer-mute) !important;
	font-size: 0.75rem !important;
	font-weight: 400;
	letter-spacing: 0.12em !important;
	margin-block: 0 0.75rem;
}
footer.wp-block-template-part .st-footer__cols ul {
	margin: 0;
	padding: 0;
	list-style: none;
	font-family: var(--st-footer-mono);
	font-size: 0.875rem;
	line-height: 2.1;
}
footer.wp-block-template-part .st-footer__cols a,
footer.wp-block-template-part .st-footer__legal a {
	text-decoration: none;
}
footer.wp-block-template-part .st-footer__cols a:hover,
footer.wp-block-template-part .st-footer__legal a:hover {
	text-decoration: underline;
	text-underline-offset: 0.2em;
}
footer.wp-block-template-part .st-footer__legal__links { order: -1; }
footer.wp-block-template-part .st-footer__legal,
footer.wp-block-template-part .st-footer__legal p { font-size: 0.75rem !important; }

@media (min-width: 1024px) {
	footer.wp-block-template-part > .wp-block-group {
		display: grid;
		grid-template-columns: minmax(12rem, 15rem) minmax(0, 1fr);
		column-gap: 3rem;
		align-items: start;
	}
	footer.wp-block-template-part > .wp-block-group > * { margin-block-start: 0; }
	footer.wp-block-template-part > .wp-block-group > .wp-block-group:first-child { grid-column: 1; grid-row: 1; }
	footer.wp-block-template-part > .wp-block-group > hr:first-of-type { display: none; }
	footer.wp-block-template-part > .wp-block-group > .st-footer__cols { grid-column: 2; grid-row: 1; }
	footer.wp-block-template-part > .wp-block-group > hr:last-of-type,
	footer.wp-block-template-part > .wp-block-group > .st-footer__legal { grid-column: 1 / -1; }
}

/* ── The chat bubble sits where it covers nothing · WB-251 ──────────────────
   The colour is chosen in inc/live-chat.php, against the surface it overlays and
   held to `--wp--custom--st-chat-min-contrast`. The BOX is here, because the
   footer has to know how much room to leave and both numbers have to be the same
   number. The helpdesk widget positions itself; these are the offsets it reads
   from, and the clearance above is computed from the same two tokens.          */
:root {
	--fs-chat-bottom: var(--wp--custom--st-chat-inset, 1.25rem);
	--fs-chat-right: var(--wp--custom--st-chat-inset, 1.25rem);
}

#freescout-w-container,
.freescout-w-button,
[id^="freescout-w"] iframe {
	bottom: var(--wp--custom--st-chat-inset, 1.25rem) !important;
	right: var(--wp--custom--st-chat-inset, 1.25rem) !important;
	max-width: calc(100vw - (var(--wp--custom--st-chat-inset, 1.25rem) * 2));
}

/* Tailwind library patterns (patterns/st-tw-*.php, TT-6159). The library puts
   text-balance on headings and text-pretty on body copy in nearly every section,
   and a heading breaks onto different lines without it. Core has no block
   support for text-wrap, so the pattern carries one of these classes.          */
.st-tw-balance {
	text-wrap: balance;
}

.st-tw-pretty {
	text-wrap: pretty;
}
