/*
Theme Name: KJ-SQUARE
Theme URI: https://shop.horatio.cn
Description: Blocksy child theme for KJ-SQUARE — dark storefront for custom mechanical keyboard parts.
Author: KJ-SQUARE
Template: blocksy
Version: 1.1.0
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: kj-square
*/

/* ============================================================
   1. Tokens
   Blocksy prints its own custom properties after every enqueued
   stylesheet, so the authoritative palette override is emitted from
   functions.php at wp_head priority 99. These are ours.
   ============================================================ */

/* ============================================================
   Typefaces (self-hosted)
   ============================================================
   Latin only, and deliberately so: the CJK faces sit at the end of the same
   stack, so Chinese falls through to the PingFang / Microsoft YaHei / Noto
   the reader's system already has. Shipping a CJK webfont would mean megabytes
   of glyphs the OS already renders well.

   Self-hosted rather than a font CDN: no third-party request, and nothing for
   the privacy policy to disclose. Both families are variable fonts, so one
   file per family covers every weight we use — Google's CSS API hands out the
   same file for each weight, which is why the per-weight downloads were byte
   for byte identical. */
@font-face {
	font-family: 'Archivo';
	src: url('assets/fonts/archivo-var-latin.woff2') format('woff2');
	font-weight: 500 800;
	font-style: normal;
	font-display: swap;
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215;
}
@font-face {
	font-family: 'Instrument Sans';
	src: url('assets/fonts/instrument-sans-var-latin.woff2') format('woff2');
	font-weight: 400 600;
	font-style: normal;
	font-display: swap;
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215;
}

/* 7-step type scale, replacing the 12 improvised font sizes the audit found */
:root {
	--kj-font-display: 'Archivo', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
	--kj-font-body: 'Instrument Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
	--fs-xs: 0.75rem;
	--fs-sm: 0.8125rem;
	--fs-base: 0.9375rem;
	--fs-md: 1.0625rem;
	--fs-lg: 1.25rem;
	--fs-xl: 1.75rem;
	--fs-2xl: 2.25rem;
}

body,
button, input, select, textarea,
.woocommerce, .wc-block-components-address-form,
.kj-nav, .kj-footer, .kj-actions {
	font-family: var(--kj-font-body);
}

h1, h2, h3, h4, h5,
.entry-title, .kj-hero__copy h1, .kj-hero__copy h2,
li.product .woocommerce-loop-product__title,
.kj-section__head h2, .kj-cats a, .kj-footer__col h4 {
	font-family: var(--kj-font-display);
	font-weight: 600;
	letter-spacing: -0.011em;
}

:root {
	--kj-bg: #18181b;
	--kj-surface: #232328;
	--kj-surface-2: #2e2e35;
	--kj-border: #3a3a42;
	--kj-border-strong: #4a4a55;
	--kj-text: #f4f4f5;
	--kj-muted: #b4b4bd;
	--kj-dim: #9a9aa5;
	--kj-accent: #ffffff;
	--kj-highlight: #8ab4ff;
	--kj-sale: #f2555a;
	--kj-new: #8ab4ff;
	--kj-radius: 10px; /* 10 not 8: percentage-width grids land on fractional columns and stair-step an 8px curve (Chromium). Same trick as translateZ anti-aliasing. */
	--kj-radius-sm: 6px;
	--kj-space: 4px;
	--kj-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
	/* One focus treatment for the whole storefront. The audit found three
	   different ones (two box-shadows at different alphas and a bare outline),
	   which is the kind of thing that reads as "unfinished" long before anyone
	   can name it. */
	--kj-focus-ring: 0 0 0 2px var(--kj-bg), 0 0 0 4px var(--kj-highlight);
	/* Reuse Blocksy's own container width so every custom band lines up with
	   the native WooCommerce content. Previously 1320px, which sat 15px wider
	   on each side than the theme's 1290px container. */
	--kj-container: var(--theme-normal-container-max-width, 1290px);
	--kj-header-h: 68px;
}

/* ============================================================
   2. Base
   ============================================================ */

body,
.ct-container-full,
.site-main,
.entry-content {
	background-color: var(--kj-bg);
	color: var(--kj-text);
}

body {
	-webkit-font-smoothing: antialiased;
}

a { color: var(--kj-text); }
a:hover { color: var(--kj-accent); }

h1, h2, h3, h4, h5, h6,
.entry-title {
	color: var(--kj-accent);
	font-weight: 800;
	letter-spacing: -0.02em;
	line-height: 1.15;
}

/* Body section heads must not out-rank the page title. Blocksy/WP were
   sizing .wp-block-heading h2 at 35px while .page-title sat at 30px. */
.entry-content > h2,
.entry-content > h3,
.entry-content > h4,
.entry-content .wp-block-heading {
	font-size: 1.375rem;
	font-weight: 700;
}
.entry-content > h3,
.entry-content > h4 { font-size: 1.125rem; }
.entry-content > h5,
.entry-content > h6 { font-size: 1rem; }

/* Document pages: paragraphs are a centered 65ch measure, but the H1 and
   section heads used to sit full-bleed at the container edge — title looked
   stranded left of the text it introduced. Same measure for both. Brands /
   categories keep their left-aligned heads (their children are grids, not
   prose), so this keys off direct paragraph/heading children only. */
.hero-section.is-width-constrained:has(+ .entry-content.is-layout-constrained > .wp-block-paragraph) .page-title,
.hero-section.is-width-constrained:has(+ .entry-content.is-layout-constrained > .wp-block-heading) .page-title,
.entry-content.is-layout-constrained > .wp-block-heading {
	max-width: var(--kj-measure);
	margin-left: auto;
	margin-right: auto;
}

/* One fixed measure for prose + document heads. 65ch scaled with each
   element's font-size (H1 30px → ~1219px, H2 22px → ~851px, p 16px → 693px),
   so three different centered columns and no shared left edge. */
:root {
	--kj-measure: 693px;
}

.entry-content p {
	color: var(--kj-muted);
	max-width: var(--kj-measure);
}
.entry-content li { max-width: var(--kj-measure); }

.screen-reader-text {
	position: absolute !important;
	width: 1px; height: 1px;
	overflow: hidden;
	clip: rect(1px, 1px, 1px, 1px);
	white-space: nowrap;
}

/* ============================================================
   3. Header
   ============================================================ */

.kj-header {
	position: sticky;
	top: 0;
	z-index: 90;
	background: rgba(8, 8, 8, 0.94);
	backdrop-filter: saturate(1.4) blur(10px);
	border-bottom: 1px solid var(--kj-border);
}

.kj-header__bar {
	display: flex;
	align-items: center;
	gap: 26px;
	max-width: var(--kj-container);
	width: var(--theme-container-width, 100%);
	height: var(--kj-header-h);
	margin: 0 auto;
}

.kj-logo {
	display: inline-flex;
	align-items: center;
	gap: 10px;
	flex: 0 0 auto;
	text-decoration: none;
}

.kj-logo__mark {
	display: grid;
	place-items: center;
	width: 34px;
	height: 34px;
	border: 1px solid var(--kj-border-strong);
	border-radius: 7px;
	color: var(--kj-accent);
}

/* Full lockup: shield + wordmark. Sized to read at header scale. */
.kj-logo img {
	display: block;
	width: auto;
	height: 46px;
}

.kj-footer__brand .kj-logo img {
	height: 58px;
}

.kj-nav { flex: 0 0 auto; }

.kj-nav__list {
	display: flex;
	align-items: center;
	gap: 24px;
	margin: 0;
	padding: 0;
	list-style: none;
}

.kj-nav__list li { position: relative; }

.kj-nav__list a {
	position: relative;
	display: inline-block;
	padding: 6px 0;
	font-size: 0.875rem;
	font-weight: 500;
	color: var(--kj-text);
	text-decoration: none;
	white-space: nowrap;
}

.kj-nav__list a::after {
	content: '';
	position: absolute;
	left: 0;
	bottom: 0;
	width: 0;
	height: 2px;
	background: var(--kj-accent);
	transition: width 0.2s ease;
}

/* Hover only underlines items that are not already current, so hovering the
   item you just clicked cannot draw a second rule. */
.kj-nav__list li:not(.current-menu-item) > a:hover::after,
.kj-nav__list .current-menu-item > a::after { width: 100%; }

.kj-nav__list .menu-item-has-children > a::before {
	content: '';
	display: inline-block;
	width: 5px; height: 5px;
	margin-right: 6px;
	border-right: 1.5px solid currentColor;
	border-bottom: 1.5px solid currentColor;
	transform: translateY(-2px) rotate(45deg);
	opacity: 0.55;
}

/* Search takes the flexible middle slot, as in the reference. */
.kj-search {
	display: flex;
	align-items: center;
	flex: 1 1 auto;
	max-width: 460px;
	margin-left: auto;
	background: var(--kj-surface);
	border: 1px solid var(--kj-border);
	border-radius: 999px;
	overflow: hidden;
}

.kj-search input[type="search"] {
	flex: 1 1 auto;
	min-width: 0;
	padding: 9px 16px;
	background: none;
	border: 0;
	color: var(--kj-text);
	font-size: 0.875rem;
	/* Blocksy puts a transition on form fields so its own focus animation can
	   run. On this field there is nothing to animate, and the transition was
	   painting a short "flash" — the background and outline visibly stepped
	   through several values on the first frames after opening the sheet.
	   Nothing to animate, nothing to flash. */
	transition: none !important;
	animation: none !important;
}

.kj-search input[type="search"]:focus,
.kj-search input[type="search"]:focus-visible {
	outline: none !important;
	box-shadow: none !important;
	/* Blocksy's focus style paints a rgba(255,255,255,.05) wash over the
	   field. On a near-black page that reads as a grey overlay sliding in
	   behind the text, and the pill around the field already marks focus. */
	background-color: transparent !important;
}

.kj-search input[type="search"]::placeholder { color: var(--kj-dim); }
.kj-search input[type="search"]:focus { outline: none; }

/* Chrome draws its own clear "x" inside `type="search"`; it sat between the
   text and the submit button and showed even on an empty field. */
.kj-search input[type="search"]::-webkit-search-cancel-button,
.kj-search input[type="search"]::-webkit-search-decoration {
	-webkit-appearance: none;
	appearance: none;
}

.kj-search button[type="submit"] {
	display: grid;
	place-items: center;
	width: 42px;
	align-self: stretch;
	background: none;
	border: 0;
	color: var(--kj-muted);
	cursor: pointer;
}

.kj-search button[type="submit"]:hover { color: var(--kj-accent); }

.kj-actions {
	display: flex;
	align-items: center;
	gap: 6px;
	flex: 0 0 auto;
}

.kj-actions__link {
	position: relative;
	display: grid;
	place-items: center;
	width: 38px;
	height: 38px;
	border-radius: 8px;
	color: var(--kj-text);
	text-decoration: none;
	transition: background-color 0.18s ease, color 0.18s ease;
}

.kj-actions__link:hover {
	background: var(--kj-surface);
	color: var(--kj-accent);
}

.kj-actions__count.is-empty,
.kj-actions__count[hidden] {
	display: none !important;
}

.kj-actions__count {
	position: absolute;
	top: 2px;
	right: 2px;
	min-width: 16px;
	height: 16px;
	padding: 0 4px;
	border-radius: 999px;
	background: var(--kj-accent);
	color: var(--kj-bg);
	font-size: 10px;
	font-weight: 700;
	line-height: 16px;
	text-align: center;
}

.kj-burger {
	display: none;
	flex-direction: column;
	gap: 5px;
	padding: 10px;
	background: none;
	border: 0;
	cursor: pointer;
}

.kj-burger span {
	display: block;
	width: 20px;
	height: 1.5px;
	background: var(--kj-text);
	transition: transform 0.2s ease, opacity 0.2s ease;
}

.kj-burger[aria-expanded="true"] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.kj-burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.kj-burger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

.kj-mobile-nav {
	padding: 8px 24px 20px;
	border-top: 1px solid var(--kj-border);
	background: var(--kj-bg);
	/* The panel is toggled with the `hidden` attribute, i.e. display:none, so a
	   transition could never run — it would just snap into place. An animation
	   restarts every time the element is displayed again, which gives the open
	   its motion for free and keeps the markup honest (still just `hidden`). */
	animation: kj-nav-drop 0.22s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

@keyframes kj-nav-drop {
	from { opacity: 0; transform: translateY(-10px); }
	to   { opacity: 1; transform: none; }
}

.kj-mobile-nav__list {
	margin: 0;
	padding: 0;
	list-style: none;
}

.kj-mobile-nav__list a {
	display: block;
	padding: 12px 0;
	border-bottom: 1px solid var(--kj-border);
	font-size: 0.9375rem;
	text-decoration: none;
}

/* The account link below the list already draws a rule; without this the last
   menu item's own border sat 16px above it as a second, stray line. */
.kj-mobile-nav__list li:last-child a { border-bottom: 0; }

.kj-search-toggle {
	display: none;
	place-items: center;
	width: 38px;
	height: 38px;
	padding: 0;
	background: transparent;
	border: 0;
	border-radius: var(--kj-radius);
	/* Matches .kj-actions__link; it was --kj-muted, which made the search icon
	   read as disabled next to the white account and cart glyphs. */
	color: var(--kj-text);
	cursor: pointer;
}

.kj-search-toggle:hover,
.kj-header.is-searching .kj-search-toggle { color: var(--kj-text); }

/* Header controls: neutralise the UA ring, then draw our own only for
   keyboard users.
   `color-scheme: dark` makes the UA focus ring light, so it has to be
   replaced. Crucially the replacement uses :focus-visible and *not* :focus:
   clicking the search icon navigates, Chrome restores focus to that button on
   the new page, and a :focus ring therefore flashed on every jump from the
   homepage. :focus-visible leaves mouse users alone and still shows a ring
   for Tab. */
.kj-search-toggle:focus,
.kj-actions__link:focus,
.kj-burger:focus,
.kj-search-close:focus,
.kj-search-clear:focus,
.kj-header.is-searching .kj-search button[type="submit"]:focus {
	outline: none;
	box-shadow: none;
}

.kj-search-toggle:focus-visible,
.kj-actions__link:focus-visible,
.kj-burger:focus-visible,
.kj-search-close:focus-visible,
.kj-search-clear:focus-visible,
.kj-header.is-searching .kj-search button[type="submit"]:focus-visible {
	outline: none;
	box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.22);
}

.kj-mobile-nav__account {
	display: flex;
	align-items: center;
	gap: 10px;
	margin-top: 16px;
	padding: 14px 4px 0;
	border-top: 1px solid var(--kj-border);
	color: var(--kj-muted);
	font-size: 0.9375rem;
	text-decoration: none;
}

.kj-mobile-nav__account:hover { color: var(--kj-text); }
.kj-mobile-nav__account svg { flex: 0 0 18px; }

/* ============================================================
   4. Footer
   ============================================================ */

.kj-footer {
	margin-top: 40px;
	border-top: 1px solid var(--kj-border);
	background: #141417;
}

.kj-footer__main {
	display: grid;
	/* Three columns: brand, then the two link groups. It used to declare five
	   for a four-child row, so one column was always empty and the whole band
	   read as misaligned. */
	grid-template-columns: minmax(0, 1.6fr) repeat(2, minmax(0, 1fr));
	gap: 40px;
	max-width: var(--kj-container);
	width: var(--theme-container-width, 100%);
	margin: 0 auto;
	padding: 56px 0 48px;
}

.kj-footer__brand p {
	max-width: 34ch;
	margin: 18px 0 0;
	font-size: 0.8125rem;
	line-height: 1.7;
	color: var(--kj-muted);
}

.kj-footer__col h4,
.kj-footer__news h4 {
	margin: 0 0 16px;
	font-size: 0.8125rem;
	font-weight: 700;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--kj-accent);
}

.kj-footer__col ul {
	margin: 0;
	padding: 0;
	list-style: none;
}

.kj-footer__col li { margin-bottom: 10px; }

.kj-footer__col a {
	font-size: 0.8125rem;
	color: var(--kj-muted);
	text-decoration: none;
	transition: color 0.16s ease;
}

.kj-footer__col a:hover { color: var(--kj-accent); }

.kj-footer__news p {
	margin: 0 0 14px;
	font-size: 0.8125rem;
	line-height: 1.6;
	color: var(--kj-muted);
}

.kj-newsletter {
	display: flex;
	align-items: center;
	background: var(--kj-surface);
	border: 1px solid var(--kj-border);
	border-radius: 999px;
	overflow: hidden;
}

.kj-newsletter input[type="email"] {
	flex: 1 1 auto;
	min-width: 0;
	padding: 10px 16px;
	background: none;
	border: 0;
	color: var(--kj-text);
	font-size: 0.8125rem;
}

.kj-newsletter input[type="email"]::placeholder { color: var(--kj-dim); }
.kj-newsletter input[type="email"]:focus { outline: none; }

.kj-newsletter button {
	display: grid;
	place-items: center;
	width: 40px;
	align-self: stretch;
	background: none;
	border: 0;
	color: var(--kj-muted);
	cursor: pointer;
}

.kj-newsletter button:hover { color: var(--kj-accent); }

.kj-footer__bottom {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 20px;
	max-width: var(--kj-container);
	width: var(--theme-container-width, 100%);
	margin: 0 auto;
	padding: 22px 0 26px;
	border-top: 1px solid var(--kj-border);
	font-size: 0.75rem;
	color: var(--kj-dim);
}

.kj-footer__bottom nav {
	display: flex;
	flex-wrap: wrap;
	gap: 18px;
}

.kj-footer__bottom a {
	color: var(--kj-dim);
	text-decoration: none;
}

.kj-footer__bottom a:hover { color: var(--kj-text); }

/* TranslatePress injects a floating switcher from JS regardless of its
   own setting; the footer carries the languages, so suppress it here. */
.trp-floating-switcher,
.trp-floater-ls {
	display: none !important;
}

/* Language switcher — globe + endonym. Native <details> supplies the open
   state and keyboard support, so there is no script to keep in sync.
   The panel anchors to `bottom: 100%`: on the last row of the page a
   top-anchored dropdown unfolds straight off the bottom of the viewport. */
.kj-lang { position: relative; }

.kj-lang > summary {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	color: var(--kj-dim);
	cursor: pointer;
	list-style: none;
	white-space: nowrap;
	transition: color 0.18s ease;
}

.kj-lang > summary::-webkit-details-marker { display: none; }
.kj-lang > summary::marker { content: none; }

.kj-lang > summary:hover,
.kj-lang[open] > summary { color: var(--kj-text); }

.kj-lang__caret { transition: transform 0.2s ease; }
.kj-lang[open] .kj-lang__caret { transform: rotate(180deg); }

.kj-to-top {
	position: fixed;
	right: 26px;
	bottom: 26px;
	z-index: 80;
	display: grid;
	place-items: center;
	width: 48px;
	height: 48px;
	border-radius: 50%;
	background: var(--kj-surface-2);
	border: 1px solid var(--kj-border-strong);
	color: var(--kj-accent);
	box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
	/* Hidden until the page is actually scrolled (see initBackToTop) — it is
	   fixed-position, so without this it sits over the hero on first paint. */
	opacity: 0;
	visibility: hidden;
	pointer-events: none;
	transform: translateY(8px);
	transition: opacity 0.25s ease, visibility 0.25s ease,
		transform 0.18s ease, background-color 0.18s ease;
}

.kj-to-top.is-visible {
	opacity: 1;
	visibility: visible;
	pointer-events: auto;
	transform: none;
}

/* Once the footer is on screen the button is in the way of the links it sits
   on top of, so it steps aside. */
body.kj-at-footer .kj-to-top {
	opacity: 0;
	visibility: hidden;
	pointer-events: none;
}

.kj-to-top:hover {
	transform: translateY(-3px);
	background: var(--kj-surface-2);
}

/* ============================================================
   5. Buttons
   ============================================================ */

.ct-button,
button,
input[type="submit"],
.button,
.wp-block-button__link,
.single_add_to_cart_button {
	border-radius: var(--kj-radius);
	font-weight: 600;
	letter-spacing: 0.02em;
	transition: background-color 0.18s ease, color 0.18s ease,
		border-color 0.18s ease, transform 0.18s ease;
}

/* Scoped deliberately: a bare `button:hover` also matched the carousel
   arrows and swatch/dot controls, overriding the translateY(-50%) that
   centres them and making them jump on hover. */
/* Entrance/press lifts removed at the client's request: only the banner
   (hero) fade-up stays. Hover keeps colour feedback only. */

.ct-button-ghost,
.button.outline {
	background-color: transparent;
	border: 1px solid var(--kj-border-strong);
	color: var(--kj-text);
}

.ct-button-ghost:hover,
.button.outline:hover {
	color: var(--kj-text);
	background-color: var(--kj-surface);
	border-color: var(--kj-accent);
}

/* ============================================================
   6. Product cards
   ============================================================ */

/* WooCommerce still ships its float-based column widths; drive the count
   from the .columns-N class instead so the grid is actually a grid. */
ul.products,
.wc-block-grid__products {
	display: grid;
	grid-template-columns: repeat(var(--kj-cols, 4), minmax(0, 1fr));
	gap: 18px;
	margin: 0;
	padding: 0;
	list-style: none;
}

ul.products.columns-1 { --kj-cols: 1; }
ul.products.columns-2 { --kj-cols: 2; }
ul.products.columns-3 { --kj-cols: 3; }
ul.products.columns-4 { --kj-cols: 4; }
ul.products.columns-5 { --kj-cols: 5; }
ul.products.columns-6 { --kj-cols: 6; }

/* WooCommerce *block* grids (product-new on the cart page, cross-sells…)
   render different markup to the [products] shortcode and ship their own
   flex layout. Force the same grid + card treatment so the two never
   disagree on the same page. */
.wc-block-grid__products {
	display: grid !important;
	grid-template-columns: repeat(var(--kj-cols, 4), minmax(0, 1fr)) !important;
	gap: 18px !important;
	margin: 0 !important;
	padding: 0 !important;
}

.wc-block-grid.has-2-columns .wc-block-grid__products { --kj-cols: 2; }
.wc-block-grid.has-3-columns .wc-block-grid__products { --kj-cols: 3; }
.wc-block-grid.has-4-columns .wc-block-grid__products { --kj-cols: 4; }
.wc-block-grid.has-5-columns .wc-block-grid__products { --kj-cols: 5; }
.wc-block-grid.has-6-columns .wc-block-grid__products { --kj-cols: 6; }

.wc-block-grid__product {
	width: auto !important;
	max-width: none !important;
	margin: 0 !important;
	text-align: left;
}

.wc-block-grid__product-title {
	font-size: 0.875rem;
}

.wc-block-grid__product-price {
	margin: 0 16px 16px;
}

/* Blocksy marks related products with both .ct-hidden-sm and .ct-hidden-md, so
   the section is hidden everywhere below ~1025px — phones and tablets alike.
   Now that every card shares one style there is no reason to withhold it;
   the markup is a plain ul.products that the grid already handles. */
@media (max-width: 1024.98px) {
	.related.products.ct-hidden-sm,
	.related.products.ct-hidden-md { display: block !important; }
}

/* Same corner cart button as the shortcode cards.
   WooCommerce's block stylesheet adds a bottom margin to .wp-block-button,
   which pushed the button ~28px up from its `bottom: 12px` anchor and landed
   it on the product title instead of the price row. */
.wc-block-grid__product .wp-block-button {
	position: absolute;
	right: 12px;
	bottom: 12px;
	margin: 0 !important;
}

.wc-block-grid__product .wp-block-button__link {
	display: grid;
	place-items: center;
	width: 30px;
	height: 30px;
	min-width: 0;
	min-height: 0;
	padding: 0;
	background: var(--kj-surface-2);
	border: 1px solid var(--kj-border-strong);
	border-radius: 5px;
	color: var(--kj-text);
	font-size: 0;
	text-indent: -9999px;
	overflow: hidden;
}

.wc-block-grid__product .wp-block-button__link::after {
	content: '';
	/* Same reasoning as the shortcode card button — see the ::after rule on
	   `li.product .button`. */
	position: absolute;
	top: 50%;
	left: 50%;
	width: 15px;
	height: 15px;
	transform: translate(-50%, -50%);
	text-indent: 0;
	background: currentColor;
	-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 5h2l2.2 10.2a1.6 1.6 0 0 0 1.6 1.3h7.6a1.6 1.6 0 0 0 1.6-1.25L21 8H6.4'/%3E%3Ccircle cx='10' cy='20' r='1.2'/%3E%3Ccircle cx='18' cy='20' r='1.2'/%3E%3C/svg%3E") center/contain no-repeat;
	mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 5h2l2.2 10.2a1.6 1.6 0 0 0 1.6 1.3h7.6a1.6 1.6 0 0 0 1.6-1.25L21 8H6.4'/%3E%3Ccircle cx='10' cy='20' r='1.2'/%3E%3Ccircle cx='18' cy='20' r='1.2'/%3E%3C/svg%3E") center/contain no-repeat;
}

.wc-block-grid__product .wp-block-button__link:hover {
	background: var(--kj-accent);
	color: var(--kj-bg);
}

.woocommerce ul.products::before,
.woocommerce ul.products::after { display: none; }

li.product,
.wc-block-grid__product {
	position: relative;
	display: flex;
	flex-direction: column;
	width: auto !important;
	float: none !important;
	margin: 0;
	background: var(--kj-surface);
	border: 1px solid var(--kj-border);
	border-radius: var(--kj-radius);
	overflow: hidden;
	transition: border-color 0.2s ease;
}

li.product:hover,
.wc-block-grid__product:hover {
	border-color: var(--kj-border-strong);
	transform: translateY(-3px);
}

/* Blocksy renders the image inside figure > a.ct-media-container, so the
   light image well has to be scoped there — putting it on
   .woocommerce-loop-product__link would also light up the title. */
li.product figure,
.wc-block-grid__product-image {
	display: block;
	margin: 0;
	background: #f4f4f4;
	overflow: hidden;
}

li.product figure a,
li.product .ct-media-container { display: block; }

li.product figure img,
li.product .ct-media-container img,
.wc-block-grid__product-image img {
	display: block;
	width: 100%;
	height: auto;
	aspect-ratio: 1 / 1 !important;
	object-fit: cover;
	transition: transform 0.3s ease;
}


li.product .woocommerce-loop-product__link {
	background: none;
	color: inherit;
	text-decoration: none;
}

li.product .woocommerce-loop-product__title,
.wc-block-grid__product-title {
	margin: 14px 16px 2px;
	padding: 0;
	font-size: 0.875rem;
	font-weight: 700;
	line-height: 1.35;
	color: var(--kj-accent);
}

/* The corner cart button is 32px wide inset 12px, so the bottom rows of text
   need ~52px of clearance or a long price ("$39.99 – $44.99") runs straight
   underneath it. Measured: price ink reached 84px past the button's left edge. */
li.product .price,
.wc-block-grid__product-price {
	/* `margin-top: auto` inside the flex-column card pins the price (and the
	   category under it) to the card's bottom edge, so a one-line title and a
	   two-line title in the same row no longer leave the category floating at
	   different heights. */
	margin: auto 16px 16px;
	padding-right: 52px;
	font-size: 0.875rem;
	font-weight: 700;
	color: var(--kj-accent);
}

li.product .entry-meta,
li.product .product-categories {
	padding-right: 52px;
	margin-right: 16px;
	margin-left: 16px;
}

/* Blocksy gives the actions row `margin-top: auto` too, so the spare space in
   a short card was split between it and the price and the category landed at
   two different heights. Let the price take all of it. */
li.product .ct-woo-card-actions {
	margin-top: 0 !important;
}

.kj-card-subtitle {
	display: block;
	margin: 0 16px 8px;
	font-size: 0.6875rem;
	line-height: 1.4;
	color: var(--kj-dim);
}

li.product .price del { color: var(--kj-dim); font-weight: 400; }
li.product .price ins { text-decoration: none; }

li.product .button,
li.product .add_to_cart_button {
	position: absolute;
	right: 12px;
	bottom: 12px;
	display: grid;
	place-items: center;
	width: 30px;
	height: 30px;
	/* Blocksy sets --theme-button-min-height on bare `button`; min-height beats
	   height, so without this the 30px width stayed 30 while the height was
	   forced to 40 and every card cart button rendered as a tall rectangle. */
	min-width: 0;
	min-height: 0;
	margin: 0;
	padding: 0;
	background: var(--kj-surface-2);
	border: 1px solid var(--kj-border-strong);
	border-radius: 5px;
	color: var(--kj-text);
	font-size: 0;
	text-indent: -9999px;
	overflow: hidden;
}

li.product .button::after,
li.product .add_to_cart_button::after {
	content: '';
	/* Absolutely centred rather than a grid item. The button still holds its
	   "Add to cart" text node, so `display: grid` treated text + ::after as
	   two grid items in two rows and pushed the icon into the lower half. */
	position: absolute;
	top: 50%;
	left: 50%;
	width: 15px;
	height: 15px;
	transform: translate(-50%, -50%);
	text-indent: 0;
	background: currentColor;
	-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 5h2l2.2 10.2a1.6 1.6 0 0 0 1.6 1.3h7.6a1.6 1.6 0 0 0 1.6-1.25L21 8H6.4'/%3E%3Ccircle cx='10' cy='20' r='1.2'/%3E%3Ccircle cx='18' cy='20' r='1.2'/%3E%3C/svg%3E") center/contain no-repeat;
	mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 5h2l2.2 10.2a1.6 1.6 0 0 0 1.6 1.3h7.6a1.6 1.6 0 0 0 1.6-1.25L21 8H6.4'/%3E%3Ccircle cx='10' cy='20' r='1.2'/%3E%3Ccircle cx='18' cy='20' r='1.2'/%3E%3C/svg%3E") center/contain no-repeat;
}

li.product .button:hover { background: var(--kj-accent); color: var(--kj-bg); }
li.product .added_to_cart { display: none; }

/* WooCommerce swaps in its own spinner via `.loading::after` while the AJAX
   add-to-cart runs. On an icon-only button that lands on top of the cart glyph
   and reads as "the cart is spinning". The loading state is worth keeping —
   it is the confirmation that the click registered — so it is expressed in
   this design's language instead: the icon dims and a ring fades in. */
li.product .button.loading::after,
.wc-block-grid__product .wp-block-button__link.loading::after {
	animation: none;
	opacity: 0.35;
}

li.product .button.loading,
.wc-block-grid__product .wp-block-button__link.loading {
	background: var(--kj-accent);
	color: var(--kj-bg);
	pointer-events: none;
}

li.product .added_to_cart,
.wc-block-grid__product .added_to_cart { display: none; }

/* Badges */
.kj-badge,
li.product .onsale,
.woocommerce span.onsale {
	position: absolute;
	top: 12px;
	left: 12px;
	z-index: 2;
	min-height: 0;
	min-width: 0;
	padding: 3px 9px;
	border-radius: 4px;
	background: var(--kj-new);
	/* Both badge colours are now light (blue #8ab4ff, red #f2555a), so the
	   label has to be dark: white on either of them lands around 2:1, while
	   near-black clears 8:1 on the blue and 5:1 on the red. */
	color: #0a0a0a;
	font-size: 0.625rem;
	font-weight: 800;
	letter-spacing: 0.08em;
	line-height: 1.6;
	text-transform: uppercase;
}

.kj-badge--sale,
li.product .onsale,
.woocommerce span.onsale { background: var(--kj-sale); }

.kj-badge--hot { background: var(--kj-sale); }

/* Star rating + review count */
li.product .star-rating,
.wc-block-grid__product-rating {
	margin: 6px 16px 16px;
	font-size: 0.6875rem;
	color: #e5c07b;
}

li.product .star-rating { float: none; }

.kj-rating {
	display: flex;
	align-items: center;
	gap: 6px;
	margin: 8px 16px 16px;
	font-size: 0.6875rem;
	color: var(--kj-muted);
}

.kj-rating__stars { color: #e5c07b; letter-spacing: 1px; }

/* ============================================================
   7. Section shell
   ============================================================ */

.kj-section {
	max-width: var(--kj-container);
	width: var(--theme-container-width, 100%);
	margin: 0 auto;
	padding: 56px 0 8px;
}

.kj-section__head {
	display: flex;
	align-items: flex-end;
	justify-content: space-between;
	gap: 24px;
	margin-bottom: 26px;
	padding-bottom: 18px;
	border-bottom: 1px solid var(--kj-border);
}

.kj-section__head h2 {
	margin: 0 0 6px;
	font-size: clamp(1.375rem, 2.2vw, 1.75rem);
	letter-spacing: 0.01em;
	text-transform: uppercase;
}

.kj-section__head p {
	margin: 0;
	font-size: 0.875rem;
	color: var(--kj-muted);
}

.kj-section__head a {
	flex: 0 0 auto;
	font-size: 0.75rem;
	font-weight: 600;
	letter-spacing: 0.06em;
	white-space: nowrap;
	color: var(--kj-muted);
	text-decoration: none;
}

.kj-section__head a:hover { color: var(--kj-accent); }

/* ============================================================
   8. Hero carousel
   ============================================================ */

/* Blocksy's own page-title hero would repeat the carousel headline. */
.home .hero-section,
.page-id-45 .hero-section {
	display: none;
}

/* Blocksy pads the content container by 60px; on the front page that shows
   up as a dark band between the sticky header and the full-bleed hero. */
.home .ct-container-full {
	padding-top: 0;
}

/* Fixed height: with the slides stacked absolutely, letting the active slide
   define the box made the whole banner grow and shrink as slides rotated. */
.kj-hero {
	position: relative;
	overflow: hidden;
	/* Full-bleed band with a contained copy: the background image runs to both
	   viewport edges while `.kj-hero__inner` keeps the text on the same
	   left edge as the product grid. This is the documented hero pattern —
	   a max-width'd inner container inside an edge-to-edge band. Containing
	   the band itself left 315px of dead space on each side at 1920px. */
	width: 100vw;
	max-width: 100vw;
	margin-inline: calc(50% - 50vw);
	height: clamp(430px, 44vw, 620px);
	background: #050505;
	border-bottom: 1px solid var(--kj-border);
}

.kj-hero__slide {
	position: absolute;
	inset: 0;
	display: grid;
	align-items: center;
	opacity: 0;
	visibility: hidden;
	transition: opacity 0.7s ease, visibility 0.7s ease;
	background-size: cover;
	background-position: center right;
}

.kj-hero__slide.is-active {
	opacity: 1;
	visibility: visible;
	flex: 1;
}

.kj-hero__slide::before {
	content: '';
	position: absolute;
	inset: 0;
	background:
		linear-gradient(90deg, rgba(5, 5, 5, 0.97) 0%, rgba(5, 5, 5, 0.86) 38%, rgba(5, 5, 5, 0.35) 68%, rgba(5, 5, 5, 0.15) 100%),
		linear-gradient(180deg, rgba(5, 5, 5, 0.5) 0%, transparent 30%, rgba(5, 5, 5, 0.75) 100%);
}

.kj-hero__inner {
	position: relative;
	z-index: 1;
	width: 100%;
	max-width: var(--kj-container);
	width: var(--theme-container-width, 100%);
	margin: 0 auto;
}

.kj-hero__copy { max-width: 620px; }

/* Reserve two lines for the blurb. Slides whose copy happens to wrap to a
   single line would otherwise shrink the copy box, and because the box is
   vertically centred in the banner the buttons moved on every rotation.
   Same-height slides are the documented fix for carousel content jumping. */
.kj-hero__copy > p:last-of-type { min-height: calc(2 * 1.65em); }

.kj-hero__eyebrow {
	margin: 0 0 14px;
	font-size: 0.6875rem;
	font-weight: 600;
	letter-spacing: 0.26em;
	text-transform: uppercase;
	color: var(--kj-muted);
}

.kj-hero h1,
.kj-hero h2 {
	margin: 0 0 18px;
	font-size: clamp(2.25rem, 5vw, 4rem);
	font-weight: 900;
	letter-spacing: -0.02em;
	line-height: 1;
	color: #fff;
}

.kj-hero h1 span,
.kj-hero h2 span {
	display: block;
	color: var(--kj-highlight);
}

.kj-hero p {
	max-width: 44ch;
	margin: 0 0 30px;
	font-size: 1rem;
	line-height: 1.65;
	color: var(--kj-muted);
}

.kj-hero__actions {
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
}

.kj-hero__dots {
	position: absolute;
	left: 50%;
	bottom: 22px;
	z-index: 3;
	display: flex;
	gap: 9px;
	transform: translateX(-50%);
}

.kj-hero__dots button {
	width: 8px;
	height: 8px;
	padding: 0;
	background: rgba(255, 255, 255, 0.28);
	border: 0;
	border-radius: 50%;
	cursor: pointer;
	transition: background-color 0.2s ease, transform 0.2s ease;
}

.kj-hero__dots button.is-active {
	background: var(--kj-accent);
	transform: scale(1.25);
}


@media (max-width: 640px) {
	.kj-promo { padding: 12px 14px; }
	.kj-promo p { letter-spacing: 0.06em; font-size: 0.8125rem; }
}

/* ============================================================
   8b. Announcement bar — header-adjacent utility strip
   (mirrors Shopify Dawn utility-bar / Keychron cc-announcement:
   full-bleed, compact, region role, sits with the header not in content)
   ============================================================ */
.kj-announcement {
	display: block;
	width: 100%;
	margin: 0;
	padding: 10px 16px;
	text-align: center;
	background: var(--kj-highlight);
	border-bottom: none;
}

.kj-announcement__message {
	margin: 0;
	font-family: var(--kj-font-body);
	font-size: 0.8125rem;
	font-weight: 600;
	line-height: 1.35;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--kj-bg);
}

/* CJK: drop the Latin tracking so 祝老板们… doesn’t look spaced-out */
:lang(zh-CN) .kj-announcement__message,
:lang(zh) .kj-announcement__message {
	letter-spacing: 0.02em;
	text-transform: none;
	font-size: 0.875rem;
}

@media (max-width: 640px) {
	.kj-announcement { padding: 8px 12px; }
	.kj-announcement__message { font-size: 0.75rem; letter-spacing: 0.04em; }
	:lang(zh-CN) .kj-announcement__message,
	:lang(zh) .kj-announcement__message { font-size: 0.8125rem; }
}

/* ============================================================
   9. Trust bar
   ============================================================ */

.kj-trust {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	max-width: var(--kj-container);
	width: var(--theme-container-width, 100%);
	margin: 0 auto;
	border-bottom: 1px solid var(--kj-border);
}

.kj-trust__item {
	display: flex;
	gap: 12px;
	align-items: center;
	padding: 22px 20px;
	/* Dividers only *between* items. Outlining every cell as well made the
	   strip read as a table rather than a row of reassurances. */
	border-left: 1px solid var(--kj-border);
}

.kj-trust__item:first-child { border-left: 0; }
.kj-trust__item:last-child { border-right: 0; }

.kj-trust__item strong {
	display: block;
	margin-bottom: 2px;
	font-size: 0.8125rem;
	font-weight: 700;
	color: var(--kj-accent);
}

.kj-trust__item span {
	font-size: 0.75rem;
	line-height: 1.45;
	color: var(--kj-muted);
}

.kj-trust__item svg {
	flex: 0 0 20px;
	stroke: var(--kj-accent);
	opacity: 0.85;
}

/* ============================================================
   10. Category cards (image on top, label + arrow below)
   ============================================================ */

ul.product-categories {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: 18px;
	margin: 0;
	padding: 0;
	list-style: none;
}

ul.product-categories li {
	position: relative;
	margin: 0;
	background: var(--kj-surface);
	border: 1px solid var(--kj-border);
	border-radius: var(--kj-radius);
	overflow: hidden;
	transition: border-color 0.2s ease;
}

ul.product-categories li:hover {
	border-color: var(--kj-border-strong);
	transform: translateY(-3px);
}

ul.product-categories li a {
	display: block;
	text-decoration: none;
}

ul.product-categories li img {
	display: block;
	width: 100%;
	aspect-ratio: 4 / 3;
	object-fit: cover;
	opacity: 0.9;
	transition: transform 0.3s ease, opacity 0.3s ease;
}

ul.product-categories li:hover img { transform: scale(1.05); opacity: 1; }

ul.product-categories li .cat-text {
	position: relative;
	display: block;
	padding: 16px 56px 18px 18px;
}

ul.product-categories li .cat-name {
	display: block;
	font-size: 0.875rem;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--kj-accent);
	/* Never split a word mid-token: narrow cards turned KEYCAPS into
	   "KEYCA / PS". */
	overflow-wrap: normal;
	word-break: keep-all;
}

ul.product-categories li .cat-desc {
	display: block;
	margin-top: 4px;
	font-size: 0.75rem;
	color: var(--kj-muted);
	overflow-wrap: normal;
	word-break: keep-all;
}

ul.product-categories li .cat-arrow {
	position: absolute;
	right: 16px;
	bottom: 16px;
	display: grid;
	place-items: center;
	width: 30px;
	height: 30px;
	border: 1px solid var(--kj-border-strong);
	border-radius: 5px;
	color: var(--kj-text);
	transition: background-color 0.18s ease, color 0.18s ease;
}

ul.product-categories li:hover .cat-arrow {
	background: var(--kj-accent);
	color: var(--kj-bg);
}

ul.product-categories li .count { display: none; }

/* ============================================================
   11. Story banner + brands + craft
   ============================================================ */

.kj-story {
	position: relative;
	max-width: var(--kj-container);
	margin: 64px auto 0;
	padding: 64px 48px;
	background: var(--kj-surface);
	border: 1px solid var(--kj-border);
	border-radius: var(--kj-radius);
	overflow: hidden;
}

.kj-story__bg {
	position: absolute;
	inset: 0;
	background-size: cover;
	background-position: center;
	opacity: 0.42;
}

.kj-story__inner { position: relative; z-index: 1; max-width: 560px; }

.kj-story h2 {
	margin: 0 0 16px;
	font-size: clamp(1.5rem, 3vw, 2.25rem);
	line-height: 1.08;
	text-transform: uppercase;
}

.kj-story p {
	margin: 0 0 26px;
	color: var(--kj-muted);
}

.kj-brands {
	display: flex;
	align-items: center;
	/* Centred rather than spread: `space-between` pinned the first and last
	   marks to the container edges and left the middle gap uneven whenever
	   the count changed. */
	justify-content: center;
	flex-wrap: wrap;
	gap: 28px 44px;
	max-width: var(--kj-container);
	width: var(--theme-container-width, 100%);
	margin: 0 auto;
}

.kj-brands__item {
	font-size: 1.0625rem;
	font-weight: 800;
	letter-spacing: 0.1em;
	/* Was #5a5a5a — 2.57:1 on this surface, the only contrast failure the
	   frozen-animation audit found. */
	color: var(--kj-muted);
	text-transform: uppercase;
	text-decoration: none;
	transition: color 0.2s ease;
}

.kj-brands__item:hover { color: var(--kj-text); }

/* /brands/ index — every brand term with its product count.
   Same language as the product cards: equal-height cards inside a row (the
   anchor used to size itself to its own copy, so a two-line description left
   its neighbours hanging with ragged bottoms — the clearest sign the page was
   not drawn by the same hand as the grid above it), Archivo for the mark so it
   matches product titles, a 4px-based gap, and the 2px lift the signature uses
   on every card. */
/* Brands page: H1 → grid was 96px (section default). Pull it to the
   same rhythm as product archives (~44px under the title band). */
body.post-type-archive.brand .entry-content,
body.tax-brand .entry-content,
.page-id-167 .entry-content {
	padding-top: 0;
}
.page-id-167 .entry-content > .kj-section,
.page-id-167 .kj-section {
	padding-top: 0;
	margin-top: 0;
}
.page-id-167 .hero-section + .entry-content,
.archive .hero-section ~ .entry-content {
	--kj-after-hero: 44px;
}
.kj-brand-index {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: 20px;
	margin: 0;
	padding: 0;
	list-style: none;
}

.kj-brand-index > li { display: flex; }

.kj-brand-index a {
	display: flex;
	flex: 1;
	flex-direction: column;
	gap: 8px;
	padding: 18px 20px;
	background: var(--kj-surface);
	border: 1px solid var(--kj-border);
	border-radius: var(--kj-radius);
	color: var(--kj-text);
	text-decoration: none;
	transition: border-color 0.18s var(--kj-ease),
		background-color 0.18s var(--kj-ease);
}

.kj-brand-index__head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 12px;
}

.kj-brand-index__desc {
	color: var(--kj-muted);
	font-size: 0.875rem;
	line-height: 1.5;
}

.kj-brand-index a:hover {
	background: var(--kj-surface-2);
	border-color: var(--kj-border-strong);
}

.kj-brand-index__name {
	font-family: var(--kj-font-display);
	font-size: 1rem;
	font-weight: 700;
	letter-spacing: 0.1em;
	text-transform: uppercase;
}

.kj-brand-index__count {
	flex: 0 0 auto;
	font-size: 0.875rem;
	font-weight: 500;
	color: var(--kj-muted);
}

@media (max-width: 1024px) {
	.kj-brand-index { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* The panel is anchored to the trigger's right edge, but on a phone the
   trigger sits at the left of the footer bar and the panel is wider than it —
   so a 130px panel ended up at left: -43, off the screen. Anchor it to the
   left there instead. */
@media (max-width: 640px) {
	.kj-lang > ul { right: auto; left: 0; min-width: 150px; }
}

@media (max-width: 640px) {
	.kj-brand-index { grid-template-columns: 1fr; }
	.kj-brand-index a { padding: 15px 16px; }
}

.kj-craft {
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: 18px;
	max-width: var(--kj-container);
	margin: 0 auto;
	padding: 56px 24px 24px;
}

.kj-craft__item {
	padding: 22px;
	background: var(--kj-surface);
	border: 1px solid var(--kj-border);
	border-radius: var(--kj-radius);
}

.kj-craft__item h3 {
	margin: 0 0 8px;
	font-size: 0.8125rem;
	letter-spacing: 0.08em;
	text-transform: uppercase;
}

.kj-craft__item p {
	margin: 0;
	font-size: 0.75rem;
	line-height: 1.6;
	color: var(--kj-muted);
}

/* ============================================================
   12. Single product
   ============================================================ */

.woocommerce-product-gallery {
	background: #f4f4f4;
	border: 1px solid var(--kj-border);
	border-radius: var(--kj-radius);
	padding: 10px;
}

/* The stock photos are shot on white studio backdrops, so the gallery keeps a
   light well and multiplies the image into it — otherwise every photo shows up
   as a white block on this near-black page.

   That blend is also why the image looked like it was zooming on hover: the
   image is multiplied against whatever sits behind it, so any backdrop change
   (a hover state on the gallery, the figure, the card) changed the image's
   appearance too — and Blocksy puts `transition: all` on the image, so every
   one of those changes was animated. The look is worth keeping; the animation
   is not. Both the image and its well are pinned here. */
.woocommerce-product-gallery,
.woocommerce-product-gallery:hover,
.woocommerce-product-gallery .ct-media-container,
.woocommerce-product-gallery .ct-media-container:hover {
	background-color: #f4f4f4;
}

.woocommerce-product-gallery img,
.woocommerce-product-gallery figure img {
	mix-blend-mode: multiply;
	transition: none !important;
	transform: none;
}

/* Last line of defence for the hover zoom. WooCommerce's block templates
   re-enable it at render time, so the CSS guarantees the magnified layer can
   never be painted even if the filter chain loses. */
.woocommerce-product-gallery .zoomImg,
.woocommerce-product-gallery img.zoomImg {
	display: none !important;
}

.woocommerce-tabs ul.tabs li a { color: var(--kj-muted); }
.woocommerce-tabs ul.tabs li.active a { color: var(--kj-accent); }

.woo-variation-swatches .variable-items-wrapper .variable-item {
	border-color: var(--kj-border-strong);
	color: var(--kj-text);
}

.woo-variation-swatches .variable-items-wrapper .variable-item.selected {
	border-color: var(--kj-accent);
	background: var(--kj-accent);
	color: var(--kj-bg);
}

/* ============================================================
   13. Cart / checkout / forms
   ============================================================ */

.woocommerce table.shop_table,
.cart_totals,
.woocommerce-checkout-review-order {
	background: var(--kj-surface);
	border: 1px solid var(--kj-border);
	border-radius: var(--kj-radius);
}

.woocommerce table.shop_table th,
.woocommerce table.shop_table td {
	border-color: var(--kj-border);
	color: var(--kj-text);
}

.cart_totals,
.woocommerce-checkout-review-order { padding: 20px; }

input[type="text"],
input[type="email"],
input[type="tel"],
input[type="password"],
input[type="search"],
input[type="number"],
textarea,
select {
	background: var(--kj-surface);
	border: 1px solid var(--kj-border);
	color: var(--kj-text);
	border-radius: var(--kj-radius);
}

input::placeholder,
textarea::placeholder { color: var(--kj-dim); }

input:focus,
textarea:focus,
select:focus { border-color: var(--kj-accent); outline: none; }

/* ============================================================
   14. 360° viewer
   ============================================================ */

.kj-360 {
	position: relative;
	margin: 16px 0;
	background: var(--kj-surface);
	border: 1px solid var(--kj-border);
	border-radius: var(--kj-radius);
	overflow: hidden;
	cursor: grab;
	touch-action: pan-y;
	user-select: none;
}

.kj-360:active { cursor: grabbing; }

.kj-360 img { display: block; width: 100%; height: auto; pointer-events: none; }

.kj-360__hint {
	position: absolute;
	left: 50%;
	bottom: 12px;
	transform: translateX(-50%);
	padding: 4px 12px;
	font-size: 11px;
	letter-spacing: 0.08em;
	text-transform: uppercase;
	color: var(--kj-muted);
	background: rgba(8, 8, 8, 0.8);
	border: 1px solid var(--kj-border);
	border-radius: 999px;
	pointer-events: none;
}

/* ============================================================
   15. Responsive
   ============================================================ */

@media (max-width: 1200px) {
	.kj-footer__main { grid-template-columns: minmax(0, 1.4fr) repeat(2, minmax(0, 1fr)); row-gap: 36px; }
}

@media (max-width: 1024px) {
	.kj-nav,
	/* The bar keeps the same three controls as desktop — search, account,
	   cart — with search collapsed to an icon that opens the input as a
	   sheet under the bar. Hiding the account link and moving search into
	   the burger menu meant phones could not reach either without opening
	   the menu first. */
	.kj-header__bar .kj-search {
		display: none;
		position: absolute;
		top: 100%;
		left: 0;
		right: 0;
		z-index: 5;
		max-width: none;
		padding: 12px 24px;
		background: rgba(8, 8, 8, 0.98);
		border-bottom: 1px solid var(--kj-border);
	}

	.kj-header.is-searching .kj-search { display: flex; }
	.kj-search-toggle { display: grid; }
	.kj-burger { display: flex; }

	/* Search, account and cart all stay in the bar. They used to be pushed
	   into the burger menu, which meant opening the menu just to reach your
	   orders — the cart sitting next to the burger made that inconsistency
	   obvious. */
	.kj-actions { margin-left: auto; }
	.kj-actions__link,
	.kj-actions__cart { display: grid; }

	.kj-trust { grid-template-columns: repeat(2, minmax(0, 1fr)); }
	/* Two-column grid below 1025px: one rule between the columns, one between
	   the rows. Drawn on a single side each — outlining both neighbours gave a
	   double line down the middle and a stray one on the outer edge. */
	.kj-trust__item { border-left: 0; }
	.kj-trust__item:nth-child(even) { border-left: 1px solid var(--kj-border); }
	.kj-trust__item:nth-child(n + 3) { border-top: 1px solid var(--kj-border); }

	/* NOTE: no `.kj-section .woocommerce` prefix here. That variant has
	   higher specificity than the plain `ul.products` used in the 640px
	   block, so it silently won and the homepage grids stayed 3-up on
	   phones (86px cards) while the shop page, which is not inside
	   .kj-section, correctly went 2-up. */
	ul.products,
	ul.products.columns-3,
	ul.products.columns-4,
	ul.products.columns-5,
	ul.products.columns-6 { --kj-cols: 3; }

	/* The block grid carries its own .has-N-columns class, so it needs the same
	   breakpoint treatment as the shortcode grid — without it the cart page's
	   "new in store" block stayed 4-up on phones and squeezed into 72px strips. */
	.wc-block-grid.has-3-columns .wc-block-grid__products,
	.wc-block-grid.has-4-columns .wc-block-grid__products,
	.wc-block-grid.has-5-columns .wc-block-grid__products,
	.wc-block-grid.has-6-columns .wc-block-grid__products { --kj-cols: 3; }

	ul.product-categories { grid-template-columns: repeat(2, minmax(0, 1fr)); }
	.kj-craft { grid-template-columns: repeat(2, minmax(0, 1fr)); }
	.kj-brands { justify-content: center; gap: 22px; }

	.kj-section__head { flex-direction: column; align-items: flex-start; gap: 10px; }
	.kj-story { padding: 48px 28px; }
}

@media (max-width: 640px) {
	.kj-header__bar { gap: 12px; }
	/* Two compact rows instead of four stacked blocks: on a phone the old
	   1-column version pushed everything below the fold. */
	.kj-trust {
		grid-template-columns: repeat(2, minmax(0, 1fr));
	}
	.kj-trust__item {
		/* Icon stays beside the text. Stacking it above cost ~26px per cell and
		   left the icon orphaned on its own line, which is what made the strip
		   look unbalanced on a phone. */
		flex-direction: row;
		align-items: flex-start;
		gap: 10px;
		padding: 15px 12px;
		border-bottom: 0;
	}
	/* Dividers are inherited from the 1024px block above. */
	.kj-trust__item strong { font-size: 0.75rem; }
	.kj-trust__item span { font-size: 0.6875rem; line-height: 1.4; }
	.kj-trust__item svg { width: 18px; height: 18px; flex: 0 0 18px; }
	/* Phones are a 4:3 canvas (4:3 for phone displays, 3:2 for smartphones in
	   the standard hero-ratio tables). The previous fixed 540px turned the
	   banner into a portrait slab — nearly twice as tall as it needed to be,
	   and far taller than the desktop banner's own ratio. */
	.kj-hero { height: auto; aspect-ratio: 4 / 3; min-height: 268px; }

	/* Blocksy sizes the page-title band with
	   `.hero-section[data-type="type-2"] > [class*="ct-container"] {
	      min-height: var(--min-height, 250px) }`
	   so the variable has to be overridden, not min-height itself.
	   250px eats a third of a phone screen before any product shows. */
	.hero-section {
		--min-height: 0;
	}
	.hero-section .entry-header {
		padding-top: 24px;
		padding-bottom: 24px;
	}
	.hero-section .page-title {
		font-size: 1.75rem;
	}

	/* Tighten the category cards so the label fits on one line. */
	ul.product-categories li .cat-text { padding: 14px 44px 16px 14px; }
	ul.product-categories li .cat-name { font-size: 0.8125rem; letter-spacing: 0.03em; }
	ul.product-categories li .cat-desc { font-size: 0.6875rem; }
	ul.product-categories li .cat-arrow {
		right: 12px;
		bottom: 12px;
		width: 26px;
		height: 26px;
	}
	ul.product-categories li .cat-arrow svg { width: 13px; height: 13px; }
	.kj-craft { grid-template-columns: 1fr; }
	ul.products,
	ul.products.columns-3,
	ul.products.columns-4,
	ul.products.columns-5,
	ul.products.columns-6 { --kj-cols: 2; }

	.wc-block-grid.has-3-columns .wc-block-grid__products,
	.wc-block-grid.has-4-columns .wc-block-grid__products,
	.wc-block-grid.has-5-columns .wc-block-grid__products,
	.wc-block-grid.has-6-columns .wc-block-grid__products { --kj-cols: 2; }
	.kj-footer__main { grid-template-columns: 1fr 1fr; padding: 40px 0 32px; }
	.kj-footer__brand { grid-column: 1 / -1; }
	.kj-footer__col:last-child { grid-column: 2; }
	.kj-footer__bottom { flex-direction: column; align-items: flex-start; gap: 12px; }
	/* Tighten the copy so the whole thing fits a 4:3 banner together with the
	   dot controls. At the desktop type scale the copy alone was taller than
	   the banner, which is why the dots landed on top of the buttons. */
	.kj-hero h1, .kj-hero h2 { font-size: clamp(1.6rem, 7vw, 2rem); }
	.kj-hero__copy .kj-hero__eyebrow { margin-bottom: 12px; }
	/* h1 *and* h2: only the first slide uses an h1 (one h1 per page), so
	   styling just h1 left the other two on Blocksy's default margin and the
	   copy box differed by 8px — which is what made the buttons jump. */
	.kj-hero__copy h1,
	.kj-hero__copy h2 { margin-bottom: 10px; }
	/* Reserve the blurb's two lines and the dot band, so the buttons can never
	   run into the dots. The slide's padding-bottom pushes the centred copy up
	   by exactly the space the dots occupy. */
	.kj-hero__copy > p:last-of-type {
		min-height: calc(2 * 1.5em);
		font-size: 0.875rem;
		line-height: 1.5;
	}
	.kj-hero__slide { padding-bottom: 34px; }
	.kj-hero__dots { bottom: 14px; }
	.kj-to-top { right: 16px; bottom: 16px; width: 42px; height: 42px; }
}

/* ============================================================
   Very narrow phones (<400px)
   ============================================================
   The bar has to hold logo + search + account + cart + burger. Measured at
   320px that came to ~320px of content inside 282px of available width, so
   the burger was pushed past the right edge. The wordmark and the gaps both
   shrink here rather than dropping a control, so phones keep the same set of
   actions as desktop. */
@media (max-width: 400px) {
	.kj-header__bar { gap: 8px; }
	.kj-logo img { max-height: 30px; }
	.kj-actions { gap: 0; }
	.kj-search-toggle,
	.kj-actions__link { width: 34px; height: 34px; }
	.kj-burger { width: 34px; margin-left: 0; }
}

/* ============================================================
   Dark-mode completeness
   ============================================================
   `color-scheme: dark` is what makes the browser render its *own* chrome —
   form controls, the <select> popup, scrollbars, autofill and text selection —
   on a dark surface. Everything below it is the handful of places WooCommerce
   still paints light, which the property cannot reach. */
:root {
	color-scheme: dark;
	accent-color: var(--kj-accent);
}

html {
	scrollbar-color: #4a4a55 var(--kj-bg);
}

::selection {
	background: var(--kj-accent);
	color: #06121f;
}

/* Form fields: the theme covers its own inputs, but WooCommerce, the
   checkout and TranslatePress each bring theirs. */
input[type="text"],
input[type="email"],
input[type="tel"],
input[type="number"],
input[type="search"],
input[type="password"],
input[type="url"],
select,
textarea,
.woocommerce form .form-row input.input-text,
.woocommerce form .form-row textarea,
.woocommerce-page form .form-row input.input-text,
.woocommerce-page form .form-row textarea {
	background-color: var(--kj-surface);
	border: 1px solid var(--kj-border-strong);
	border-radius: var(--kj-radius);
	color: var(--kj-text);
}

input::placeholder,
textarea::placeholder { color: var(--kj-muted); }

/* Chrome paints autofilled fields light yellow and ignores background-color. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
select:-webkit-autofill {
	-webkit-text-fill-color: var(--kj-text);
	-webkit-box-shadow: 0 0 0 1000px var(--kj-surface) inset;
	caret-color: var(--kj-text);
}

/* WooCommerce notices, including the "nothing matched your search" block,
   which ships as a white panel on a dark page.

   `.woocommerce-no-products-found` is only a wrapper around a
   `.woocommerce-info`, so both were styled and the result was two nested
   panels with a stray empty row. Only the inner notice is painted now.

   The left accent stripe is grey rather than `--kj-accent`, which resolves to
   #ffffff here and read as a bright white bar clamped to the edge. */
.woocommerce-info,
.woocommerce-message,
.woocommerce-error,
.woocommerce-noreviews,
p.no-comments {
	background-color: var(--kj-surface);
	border: 1px solid var(--kj-border-strong);
	border-left: 3px solid var(--kj-border-strong);
	border-radius: var(--kj-radius);
	color: var(--kj-text);
}

.woocommerce-no-products-found {
	background: none;
	border: 0;
	padding: 0;
	margin: 0;
}

.woocommerce-info::before,
.woocommerce-message::before,
.woocommerce-error::before { color: var(--kj-accent); }

.woocommerce-info a,
.woocommerce-message a,
.woocommerce-error a { color: var(--kj-accent); }

/* The empty-cart / search-results blocks */
.woocommerce .cart-empty,
.woocommerce-page .cart-empty,
.woocommerce .return-to-shop { color: var(--kj-text); }

.woocommerce table.shop_table,
.woocommerce table.shop_table th,
.woocommerce table.shop_table td {
	background: transparent;
	border-color: var(--kj-border);
	color: var(--kj-text);
}

/* Quantity steppers and the coupon row */
.woocommerce .quantity input.qty {
	background: var(--kj-surface);
	border: 1px solid var(--kj-border-strong);
	color: var(--kj-text);
	border-radius: var(--kj-radius);
}

/* ============================================================
   Header search sheet (phones and tablets only)
   ============================================================
   The desktop bar shows the input inline and never opens a sheet, so every
   rule here is scoped below 1025px. Without the scope they leaked onto the
   desktop form: the close button appeared after the submit for no reason,
   and the input picked up panel padding that read as an overlay. */
/* No close button in the sheet. The desktop field has none either — it is
   always on screen — and the sheet is dismissed by tapping the search icon
   again, which is the control that opened it. A third button in the row was
   the thing making it read as clutter rather than a search bar. */

@media (max-width: 1024px) {
	/* Same specificity as the hide rule above, so document order decides. */
	.kj-search .kj-search-close {
		display: grid;
		place-items: center;
		width: 40px;
		height: 40px;
		padding: 0;
		background: transparent;
		border: 1px solid var(--kj-border-strong);
		border-radius: var(--kj-radius);
		color: var(--kj-muted);
		cursor: pointer;
	}

	.kj-search .kj-search-close:hover { color: var(--kj-text); }

	.kj-header.is-searching .kj-search {
		display: flex;
		align-items: center;
		gap: 10px;
	}

	.kj-header.is-searching .kj-search input[type="search"] {
		flex: 1 1 auto;
		min-width: 0;
		padding: 13px 14px;
		background: var(--kj-surface);
		border: 1px solid var(--kj-border-strong);
		border-radius: var(--kj-radius);
		color: var(--kj-text);
		font-size: 0.9375rem;
	}

	.kj-header.is-searching .kj-search input[type="search"]:focus {
		border-color: var(--kj-accent);
		outline: none;
	}

	/* The sheet takes part in the layout instead of floating over the page.
	   As an overlay it sat on top of the hero, hiding the headline behind it;
	   here the sheet becomes a second row and everything below is pushed down
	   rather than covered.
	   Grid rather than flex-wrap: wrapping re-flows the *existing* row when a
	   second line appears, which is what nudged the burger sideways. Named
	   areas pin the first row's placement exactly, so the sheet is purely an
	   extra row that nothing else can be displaced by. */
	.kj-header.is-searching .kj-header__bar {
		display: grid;
		grid-template-columns: auto auto 1fr auto auto;
		grid-template-areas:
			"logo nav . actions burger"
			"search search search search search";
		align-items: center;
		height: auto;
		padding-bottom: 0;
		/* The bar's base rule sets `gap`, which becomes a row gap in grid
		   mode and opened an extra 8px band above the sheet. */
		row-gap: 0;
	}

	/* Every visible child needs its own area. Anything left unplaced is
	   auto-placed into an implicit row, which is what gave the tablet layout
	   an extra row and moved the second row's contents up. */
	.kj-header.is-searching .kj-logo { grid-area: logo; }
	.kj-header.is-searching .kj-nav { grid-area: nav; }
	.kj-header.is-searching .kj-actions { grid-area: actions; margin-left: 0; }

	/* The 68px row height lives on the bar and is lost when the bar grows to
	   `auto`; putting it back on the row's children keeps the row exactly as
	   tall as before while the sheet still adds a second row below. */
	.kj-header.is-searching .kj-header__bar > :not(.kj-search) {
		min-height: var(--kj-header-h);
	}

	/* The burger is a flex *column* of three bars, so a taller box pushes the
	   lines to the top rather than centring them. Vertically only: centring
	   them horizontally too slides the 20px bars 3px left inside their box. */
	.kj-header.is-searching .kj-burger {
		grid-area: burger;
		margin-left: 0;
		justify-content: center;
	}

	.kj-header.is-searching .kj-search {
		grid-area: search;
		display: flex;
		align-items: center;
		gap: 10px;
		position: static;
		/* Full-bleed row: the bar is centred but width-constrained, so a
		   negative inline margin stretches the sheet to both viewport edges. */
		width: auto;
		max-width: none;
		margin-inline: calc(50% - 50vw);
		/* No top margin: it stacked on top of the sheet's own padding and
		   pushed the header's bottom rule ~20px further down than the sheet
		   needed, which read as the navbar growing a band of empty space. */
		margin-top: 0;
		padding: 8px 20px 10px;
		background: transparent;
		border: 0;
		border-top: 1px solid var(--kj-border);
		border-radius: 0;
	}

	.kj-header.is-searching .kj-search input[type="search"] {
		flex: 1 1 auto;
		min-width: 0;
		height: 44px;
		padding: 0 16px 0 42px;
		/* Tint only — no border at all. An accent border on focus is what put
		   a bright white outline around the field: --kj-accent is #ffffff on
		   this theme, and the field is auto-focused the moment the sheet
		   opens. */
		background-color: var(--kj-surface);
		background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238a8a8a' stroke-width='1.9' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='6.5'/%3E%3Cpath d='M16 16l4.5 4.5'/%3E%3C/svg%3E");
		background-repeat: no-repeat;
		background-position: left 14px center;
		background-size: 17px 17px;
		border: 0;
		border-radius: 999px;
		color: var(--kj-text);
		font-size: 0.9375rem;
	}

	.kj-header.is-searching .kj-search input[type="search"]:focus,
	.kj-header.is-searching .kj-search input[type="search"]:focus-visible {
		border: 0;
		outline: none;
		box-shadow: none;
		background-color: var(--kj-surface) !important;
	}

	/* One circular confirm and one flat dismiss, both borderless. */
	.kj-header.is-searching .kj-search button[type="submit"] {
		display: grid;
		place-items: center;
		flex: 0 0 auto;
		width: 44px;
		height: 44px;
		padding: 0;
		background: var(--kj-surface);
		border: 0;
		border-radius: 50%;
		color: var(--kj-text);
		cursor: pointer;
	}

	.kj-header.is-searching .kj-search button[type="submit"]:active { background: var(--kj-surface-2); }

	.kj-search .kj-search-close {
		display: grid;
		place-items: center;
		flex: 0 0 auto;
		width: 40px;
		height: 40px;
		padding: 0;
		background: transparent;
		border: 0;
		color: var(--kj-muted);
		cursor: pointer;
	}

	.kj-search .kj-search-close:hover { color: var(--kj-text); }

	/* Sits inside the pill, clear of its right edge. */
	.kj-search .kj-search-clear {
		position: absolute;
		right: 20px;
		top: 50%;
		width: 30px;
		height: 30px;
		margin-top: -15px;
		transform: translateX(calc(-1 * (100vw - 100%)));
	}

	.kj-header.is-searching .kj-search input[type="search"]::-webkit-search-cancel-button {
		-webkit-appearance: none;
		appearance: none;
	}
}

/* ============================================================
   Micro-interactions
   ============================================================
   Deliberately small: a zoom on the card image and a press response on
   controls. Timings sit in the 180–320ms band the guidance recommends, and
   every one of them is wrapped in the reduced-motion query below. */
li.product .ct-media-container img,
li.product figure img {
	transition: transform 0.32s cubic-bezier(0.22, 0.61, 0.36, 1);
}

@media (hover: hover) and (pointer: fine) {

	li.product:hover .cat-arrow,
	li.product:hover .button { border-color: var(--kj-accent); }
}

.kj-cats a img {
	transition: transform 0.32s cubic-bezier(0.22, 0.61, 0.36, 1);
}

@media (hover: hover) and (pointer: fine) {
	ul.product-categories li:hover a img { transform: scale(1.05); }
}

/* Press feedback on the primary controls */
.ct-button:active,
.single_add_to_cart_button:active,
.kj-search-toggle:active,
.kj-actions__link:active { transform: scale(0.96); }

.kj-actions__link,
.kj-search-toggle,
.kj-burger { transition: transform 0.16s ease, color 0.18s ease, background-color 0.18s ease, opacity 0.25s ease; }

/* The hero dots get a slightly springy active state */
.kj-hero__dots button { transition: background-color 0.2s ease, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); }

/* Respect the OS setting: nothing above should move for someone who asked
   the system for less motion. */
/* The hero has no scroll to wait for, so it animates on first paint. The
   `both` fill holds the transparent start state during the delay, which is
   what stops the copy from flashing at full opacity before it rises. */
@keyframes kj-fade-up {
	from { opacity: 0; transform: translate3d(0, 20px, 0); }
	to   { opacity: 1; transform: none; }
}

.kj-hero__copy > * {
	animation: kj-fade-up 0.7s cubic-bezier(0.22, 0.61, 0.36, 1) both;
}

.kj-hero__copy > *:nth-child(1) { animation-delay: 0.05s; }
.kj-hero__copy > *:nth-child(2) { animation-delay: 0.13s; }
.kj-hero__copy > *:nth-child(3) { animation-delay: 0.21s; }
.kj-hero__copy > *:nth-child(n + 4) { animation-delay: 0.29s; }

/* ============================================================
   My Account — register as a button, not a second column
   ============================================================
   Two full forms side by side read as two separate destinations, when
   registering is really the alternative to signing in. The register column is
   collapsed with the `hidden` attribute (set in script, so without JS the
   account page behaves exactly as WooCommerce ships it) and reached from a
   button sitting beside “lost password?”. */
.u-column2[hidden] { display: none !important; }

/* The dashed rule between the two columns is left over from the old layout
   and would otherwise hang in mid-air beside a single column. */
.u-columns.kj-register-collapsed .u-column1 { border-right: 0; }

.kj-register-toggle {
	display: inline;
	padding: 0;
	background: none;
	border: 0;
	color: var(--kj-text);
	font: inherit;
	font-weight: 600;
	cursor: pointer;
	text-decoration: underline;
	text-underline-offset: 3px;
}

.kj-register-toggle:hover { color: var(--kj-dim); }

.woocommerce-form-login .lost_password {
	display: flex;
	align-items: baseline;
	gap: 20px;
}

/* ============================================================
   My Account — the same frame, signed in or not
   ============================================================
   The signed-out dashboard is drawn with the same two wrapper classes
   WooCommerce uses when someone is signed in, so the navigation, spacing and
   typography all come from the existing account styling rather than being
   rebuilt. Only the column geometry needs help: with one form there is no
   second column to sit beside, so it centres instead of hugging the left. */
/* Signed out, the login form is the whole of the content column, so it centres
   inside it instead of hugging the left of a two-column row that does not
   exist. The frame itself — including the flex parent that puts the navigation
   beside the content — is identical in both states; only this differs. */
.kj-guest .woocommerce-MyAccount-content .u-columns {
	display: flex;
	justify-content: center;
}

/* Blocksy reserves the gap between the two account columns with padding on
   their inner edge rather than with a gap: the right-hand column carries
   `padding-left: 40px`. With a single column there is nothing to reserve, and
   leaving it in pushed the register form 40px inside its column while the
   login form sat flush — the two then did not line up. */
.kj-guest .woocommerce-MyAccount-content .u-column1,
.kj-register-only .u-column2 {
	width: 100%;
	max-width: 460px;
	/* !important because the 40px comes from Blocksy's generated stylesheet,
	   which is emitted after this file; matching its selector would mean
	   copying its customiser output rather than describing the intent. */
	padding-left: 0 !important;
	padding-right: 0 !important;
}

/* The register heading sits in the same centred column as the form it labels,
   so the two share a left edge — the login page gets that for free because
   WooCommerce prints its heading inside the column. The tracking page is left
   alone: its form spans the full width, so a flush heading matches it. */
.kj-register-only > h2 {
	width: 100%;
	max-width: 460px;
	margin-inline: auto;
}

/* The register page is the same template with the login half taken out. The
   form's own heading goes too: the page title already says "注册", so keeping
   both printed the word twice in a row. */
.kj-register-only .u-column1 { display: none; }
.kj-register-only .u-column2 > h2 { display: none; }
.kj-register-only .u-columns { display: flex; justify-content: center; }

.kj-guest-orders__list {
	margin: 0;
	padding: 0;
	list-style: none;
	border-top: 1px solid var(--kj-border);
}

.kj-guest-orders__list li { border-bottom: 1px solid var(--kj-border); }

.kj-guest-orders__list a {
	display: block;
	padding: 12px 0;
	color: var(--kj-text);
	text-decoration: none;
}

.kj-guest-orders__list a:hover { text-decoration: underline; text-underline-offset: 3px; }

.kj-guest-orders__hint { margin-top: 18px; font-size: 0.875rem; }

/* ============================================================
   Product attribute table zebra striping
   ============================================================
   Blocksy stripes even rows of the attributes table with a translucent light
   grey, which is invisible on a light theme and a grey band on this one — the
   middle row of a three-row table was the only one that looked wrong. The
   stripe is kept, because it does help a long table, but painted from the
   surface tokens instead. !important because the value comes from Blocksy's
   generated stylesheet, which is printed after this file. */
.woocommerce-product-attributes tr:nth-child(even) th,
.woocommerce-product-attributes tr:nth-child(even) td,
.shop_attributes tr:nth-child(even) th,
.shop_attributes tr:nth-child(even) td {
	background-color: rgba(255, 255, 255, 0.05) !important;
}

/* ============================================================
   Order-received notice + block-checkout dropdowns
   ============================================================
   The "thank you, your order has been received" line is a classic
   `.woocommerce-notice--success`, which kept WooCommerce's light default
   (#f0f1f3) and read as a pale grey slab on this page. The state/country
   pickers in the block checkout are custom popup DOM rather than a native
   <select>, so `color-scheme: dark` never reaches them and they opened white
   on white — they are painted here instead. */
.woocommerce-notice,
.woocommerce-notice--success,
.woocommerce-thankyou-order-received {
	background-color: var(--kj-surface-2);
	border: 1px solid var(--kj-border-strong);
	border-left-width: 3px;
	color: var(--kj-text);
}

.wc-block-components-combobox-control__dropdown,
.components-popover__content,
[role="listbox"] {
	background-color: var(--kj-surface);
	border: 1px solid var(--kj-border-strong);
	border-radius: var(--kj-radius);
	color: var(--kj-text);
	box-shadow: 0 12px 34px rgba(0, 0, 0, 0.55);
}

/* A transparent background makes Chrome fall back to a light native popup no
   matter what `color-scheme` says, which is why the state/country pickers
   opened white on white. A solid fill is what fixes it. */
.wc-block-components-address-form select,
.wc-block-components-select select,
.wc-block-components-combobox-control input,
.wc-block-components-combobox-control select {
	background-color: var(--kj-surface);
	color: var(--kj-text);
}

[role="option"],
.wc-block-components-combobox-control__dropdown [role="option"] {
	background-color: transparent;
	color: var(--kj-text);
}

[role="option"]:hover,
[role="option"][aria-selected="true"],
.wc-block-components-combobox-control__dropdown [role="option"]:hover,
.wc-block-components-combobox-control__dropdown [role="option"][aria-selected="true"] {
	background-color: var(--kj-surface-2);
	color: var(--kj-text);
}

/* ============================================================
   Variation swatches (woo-variation-swatches)
   ============================================================
   The plugin exposes its whole appearance as CSS custom properties, so the
   light default — white chips with black text — is retuned by setting those
   variables rather than by out-specifying its selectors. Declared on the
   swatch wrapper as well as :root so it wins wherever the plugin declares
   its own defaults. Colour swatches keep their inline background, which is
   the point of them; only the ring and the states come from here. */
:root,
.woo-variation-items-wrapper,
.woo-variation-swatches {
	--wvs-item-background-color: var(--kj-surface);
	--wvs-item-text-color: var(--kj-text);
	--wvs-item-box-shadow: none;
	--wvs-hover-item-background-color: var(--kj-surface-2);
	--wvs-hover-item-text-color: var(--kj-text);
	--wvs-hover-item-box-shadow: none;
	--wvs-selected-item-background-color: var(--kj-surface-2);
	--wvs-selected-item-text-color: var(--kj-text);
	--wvs-selected-item-box-shadow: none;
	--wvs-single-product-item-width: 44px;
	--wvs-single-product-item-height: 40px;
	--wvs-single-product-item-font-size: 14px;
	--wvs-tooltip-background-color: var(--kj-surface-2);
	--wvs-tooltip-text-color: var(--kj-text);
}

/* The ring is a border, which the plugin's variables do not cover. The radius
   needs !important: the plugin sets it from a four-class selector
   (`.woo-variation-swatches .variable-items-wrapper .variable-item:not(
   .radio-variable-item).button-variable-item`), which nothing reasonable
   should have to out-rank. */
.woo-variation-items-wrapper .variable-item {
	border: 1px solid var(--kj-border-strong);
	border-radius: var(--kj-radius) !important;
}

.woo-variation-items-wrapper .variable-item.selected,
.woo-variation-items-wrapper .variable-item[aria-checked="true"] {
	border-color: var(--kj-text);
}

@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation-duration: 0.001ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.001ms !important;
		scroll-behavior: auto !important;
	}

	li.product:hover .ct-media-container img,
	li.product:hover figure img,
	ul.product-categories li:hover a img { transform: none; }

	/* Reduced motion keeps the content, drops the movement: no offset, no
	   wait, nothing that fades in after the reader has started reading. */
	.kj-hero__copy > * { animation: none; opacity: 1; transform: none; }
}

/* ============================================================
   WooCommerce sort dropdown
   ============================================================
   Blocksy resets every select with `appearance: none`, but only supplies its
   own replacement icon below 1025px (`.ct-sort-icon`). On desktop that left a
   bare bordered box with no affordance at all, so the control is restyled
   here: surface background, proper chevron, and an accent border on hover.
   The arrow colour is baked into the data URI because `currentColor` cannot
   be used inside a background-image. */
.woocommerce-ordering select {
	-webkit-appearance: none;
	appearance: none;
	height: 40px;
	min-width: 170px;
	padding: 0 38px 0 14px;
	background-color: var(--kj-surface);
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ededed' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
	background-repeat: no-repeat;
	background-position: right 13px center;
	background-size: 15px 15px;
	border: 1px solid var(--kj-border-strong);
	border-radius: var(--kj-radius);
	color: var(--kj-text);
	font-size: 0.875rem;
	cursor: pointer;
	transition: border-color 0.18s ease, background-color 0.18s ease;
}

.woocommerce-ordering select:hover,
.woocommerce-ordering select:focus {
	background-color: var(--kj-surface-2);
	border-color: var(--kj-accent);
	outline: none;
}

/* The open list. Chrome honours `color-scheme: dark` on its own; Firefox
   needs the options spelled out. */
.woocommerce-ordering select option {
	background-color: var(--kj-surface);
	color: var(--kj-text);
}

/* Below 1025px Blocksy collapses the control to a 34px icon button with its
   own overlay glyph, so the chevron and padding must stand down or they fight
   it for space. */
@media (max-width: 1024px) {
	.woocommerce-ordering select {
		min-width: 0;
		height: 34px;
		padding: 0;
		background-image: none;
	}
	.woocommerce-ordering select:hover,
	.woocommerce-ordering select:focus { background-color: transparent; }
}

/* ============================================================
   Search field: focus ring and clear button
   ============================================================
   `color-scheme: dark` makes the browser's default focus ring light, which
   flashed as a white outline the moment the field was focused. Both :focus
   and :focus-visible are covered so no UA style can slip through, and the
   ring is replaced with the accent used elsewhere in the UI. */
.kj-search input[type="search"]:focus {
	outline: none;
	box-shadow: none;
}

/* Chrome matches :focus-visible on text inputs even for *mouse* clicks — it
   assumes you need to see where you are about to type — so a ring drawn there
   appeared as a selection box on every click. It is removed entirely and the
   field signals focus with a slightly lighter surface instead: still a
   visible focus indicator (WCAG 2.4.7) alongside the caret, without the box.
   `outline` is reset as well, because the UA's own :focus-visible ring would
   otherwise take its place once ours is gone. */
.kj-search input[type="search"]:focus,
.kj-search input[type="search"]:focus-visible {
	outline: none !important;
	box-shadow: none !important;
	border-color: transparent;
	background-color: rgba(255, 255, 255, 0.06);
}

.kj-header.is-searching .kj-search input[type="search"]:focus-visible {
	outline: none;
	box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.22);
}

/* Chrome's own clear affordance was removed because it showed on an empty
   field and sat in the wrong place; this one only appears once there is
   something to clear. */
.kj-search-clear {
	display: grid;
	place-items: center;
	flex: 0 0 auto;
	width: 28px;
	height: 28px;
	padding: 0;
	background: transparent;
	border: 0;
	border-radius: 50%;
	color: var(--kj-muted);
	cursor: pointer;
	transition: color 0.16s ease, background-color 0.16s ease;
}

.kj-search-clear[hidden] { display: none; }
.kj-search-clear:hover { color: var(--kj-text); }

/* ============================================================
   Language dropdown
   ============================================================
   Restyled to match the notice and card surfaces: same radius, same border,
   an accent bar on the active row instead of colour alone. */
.kj-lang > ul {
	position: absolute;
	right: 0;
	bottom: calc(100% + 8px);
	z-index: 90;
	min-width: 160px;
	margin: 0;
	padding: 6px;
	list-style: none;
	background: var(--kj-surface);
	border: 1px solid var(--kj-border-strong);
	border-radius: 10px;
	box-shadow: 0 18px 40px rgba(0, 0, 0, 0.65);
}

/* details/ul/li otherwise paint UA disc markers — the two language rows
   each got a bullet in front. */
.kj-lang,
.kj-lang > ul,
.kj-lang > ul li {
	list-style: none;
}
.kj-lang > ul li::marker,
.kj-lang > ul a::marker {
	content: none;
	font-size: 0;
}

.kj-lang > ul a,
.kj-lang > ul a:link,
.kj-lang > ul a:visited {
	position: relative;
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 10px 12px;
	border-radius: 7px;
	color: var(--kj-text);
	font-size: 0.875rem;
	text-decoration: none;
	transition: background-color 0.16s ease, padding-left 0.16s ease;
}

.kj-lang > ul a:hover { background: var(--kj-surface-2); padding-left: 16px; }

.kj-lang > ul a.is-current {
	padding-left: 16px;
	color: var(--kj-accent);
	font-weight: 600;
}

/* A short accent bar marks the current language, so the state is not carried
   by colour alone. */
.kj-lang > ul a.is-current::before {
	content: '';
	position: absolute;
	left: 6px;
	top: 50%;
	width: 3px;
	height: 16px;
	margin-top: -8px;
	border-radius: 2px;
	background: currentColor;
}

/* ============================================================
   Search with no results
   ============================================================
   Blocksy marks its "nothing matched" explanation `ct-hidden-sm`, so phones
   showed only the title with no reason given. It is unhidden here, and the
   WooCommerce notice that repeated the same thing is removed in functions.php
   — leaving one message, the same one, at every width. */
/* Blocksy's rule is `.ct-hidden-sm { display: none !important }`, so a plain
   override loses no matter how specific it is. */
body.search-no-results .page-description.ct-hidden-sm { display: block !important; }

/* ============================================================
   Contrast repairs found by the site-wide audit
   ============================================================
   WooCommerce's own notices print buttons *inside* the notice, and the notice
   sets a text colour that cascades into them. On this theme that colour is
   near-white while the button's own background is white, so "Confirm email
   address" / "Browse products" rendered as white on white — a 1:1 pair, i.e.
   invisible. Restoring the button's own colours fixes every notice-embedded
   button at once rather than the two that happened to be reported.
   Comes from WooCommerce core:
     src/Internal/CustomerEmailVerification/VerificationController.php
     templates/myaccount/orders.php (wc_print_notice with an <a class="button">) */
.woocommerce-notice .button,
.woocommerce-message .button,
.woocommerce-info .button,
.woocommerce-error .button,
.wc-block-components-notice-banner .button,
.wc-block-components-notice-banner a {
	background-color: #ffffff;
	color: #0a0a0a;
}

/* Nested children carry their own `color`, so setting it on the anchor alone
   left a white span sitting on the white button. Inherit closes that. */
.woocommerce-notice .button *,
.woocommerce-message .button *,
.woocommerce-info .button *,
.woocommerce-error .button *,
.wc-block-components-notice-banner .button *,
.wc-block-components-notice-banner a * {
	color: inherit;
	background-color: transparent;
}

/* The required-field asterisk ships in two reds from two sources — #cc0000 and
   #ff0000 — and both land around 3:1 on this background. `!important` because
   the winning rule comes from the WooCommerce Blocks stylesheet, which is
   emitted after this file. */
span.required,
abbr.required,
.wc-block-components-text-input .required {
	color: #ff8a8a !important;
}

/* ============================================================
   Component polish
   ============================================================
   Everything here is derived from the tokens above — no component invents its
   own radius, duration or easing. :where() keeps the focus rule at zero
   specificity so it acts as a floor: the header controls and the search field
   keep the bespoke rings they already had for good reason. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
	outline: none;
	box-shadow: var(--kj-focus-ring);
}

/* Buttons: one geometry, one easing. Scoped to the WooCommerce and Blocks
   classes actually in use so the icon-only controls keep their own sizing. */
.woocommerce a.button:not(.add_to_cart_button),
.woocommerce button.button,
.woocommerce input.button,
.wc-block-components-button,
.wp-element-button,
.wc-block-components-notice-banner .button {
	min-height: 44px;
	padding: 0 20px;
	border-radius: var(--kj-radius-sm);
	font-family: var(--kj-font-body);
	font-weight: 600;
	letter-spacing: 0.01em;
	transition: background-color 0.18s var(--kj-ease),
		color 0.18s var(--kj-ease),
		transform 0.12s var(--kj-ease);
}

.woocommerce a.button:not(.add_to_cart_button):active,
.woocommerce button.button:active,
.wc-block-components-button:active,
.wp-element-button:active {
	transform: translateY(1px);
}

/* Inputs and selects: one height, one radius, one focus treatment.
   WC Blocks emits its stylesheet after ours and paints text inputs
   transparent with a 3px radius — same !important rationale as the asterisk fix. */
.woocommerce input.input-text,
.woocommerce textarea,
.woocommerce select,
.wc-block-components-text-input input,
.wc-block-components-address-form input[type="text"],
.wc-block-components-address-form input[type="email"],
.wc-block-components-address-form input[type="tel"],
.wc-block-components-address-form input[type="number"],
.wc-block-components-select select,
.wc-block-components-combobox-control input,
.wc-block-components-combobox-control select {
	min-height: 44px;
	border-radius: var(--kj-radius-sm) !important;
	background-color: var(--kj-surface) !important;
	color: var(--kj-text) !important;
	transition: border-color 0.18s var(--kj-ease), background-color 0.18s var(--kj-ease);
}

/* ------------------------------------------------------------------
   Signature: the card's "key press"
   ------------------------------------------------------------------
   The store sells mechanical switches, so the product card answers a hover the
   way a switch does — the card settles a hair closer to the page and the
   add-to-cart control takes a 1px press, as if bottoming out. It is the one
   place this design raises its voice; everything else stays quiet. */
li.product {
	transition: background-color 0.18s var(--kj-ease), border-color 0.18s var(--kj-ease);
}

li.product .add_to_cart_button,
li.product .button {
	transition: background-color 0.18s var(--kj-ease),
		border-color 0.18s var(--kj-ease);
}

/* ============================================================
   Fixes from the second visual pass
   ============================================================ */

/* ① The rounded card corner was showing a white arc. The image well is a light
   grey (deliberate — the stock photos are shot on white), but the image inside
   it is square, so where the card rounds off, the well's corner peeked through.
   Inheriting the card's radius closes the seam. */
li.product > figure,
li.product .ct-media-container,
li.product > figure img,
li.product .ct-media-container img,
.wc-block-grid__product-image {
	/* One layer does the rounding: the card itself (radius + overflow:hidden).
	   Giving the well, the anchor and the image their own copies of the same
	   radius stacks three antialiased edges on top of each other, which is what
	   left a light fringe along the corner — made worse because the well behind
	   is a light grey. Single clip, single edge. */
	border-radius: 0;
}

/* ② The homepage brands row is very short, and three separate spacings stacked
   up between it and the footer (8 + 40 + 60 = 109px), which read as a band of
   empty space rather than as breathing room. Trimmed, homepage only. */
.home .ct-container-full { padding-bottom: 28px; }
.home .kj-footer { margin-top: 16px; }

/* ③ Brand descriptions were 12–13px. The ratios passed (7.4:1, AAA) but at that
   size on a grey ground the text still muddled together, so it is set a step
   larger and a touch heavier instead. */
.kj-brand-index__desc,
.kj-brand-index__count {
	font-size: 0.875rem;
	font-weight: 500;
	color: var(--kj-muted);
}

/* ④ Add-to-cart: turn the button into a check *in place*.
   WooCommerce's AJAX handler appends a separate `.added_to_cart` link and hides
   the original button, so the control changes element mid-interaction — and no
   amount of repositioning makes a different element sit exactly where the old
   one was. The link is removed from view entirely and the button keeps its slot;
   only its glyph changes. Nothing moves. */
li.product .added_to_cart { display: none !important; }

/* The actions row aligns on the baseline, and the button's baseline moves the
   moment its contents change — which is what kept the checked button a pixel
   above its neighbours. Centring the row removes the dependency entirely. */
li.product .ct-woo-card-actions { align-items: center; }

li.product .add_to_cart_button.added {
	/* WooCommerce hides the button the moment it appends its "View cart" link.
	   That hidden button *is* the slot we want to keep, so it is brought back
	   and the link is what gets removed instead. */
	/* `grid`, matching the button's own display: switching to flex shifted the
	   glyph by a pixel because the two box modes sit differently on the parent
	   row's baseline. Same box, same place. */
	display: grid !important;
	place-items: center;
	/* The label is hidden by the same mechanism the resting button already
	   uses (an off-canvas text indent in the theme), not by font-size: 0 —
	   overriding the font size changed the button's line box and nudged it a
	   pixel off its neighbours. */
	/* The `.added` class otherwise picks up a plain white button style, so the
	   fill is pinned back to the same token the resting button uses. */
	background-color: var(--kj-surface-2) !important;
	background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f4f4f5' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E") !important;
	background-repeat: no-repeat;
	background-position: center;
	background-size: 15px 15px;
}

/* Whatever the cart glyph was made of, it goes — but without `display: none`.
   Removing it from layout changed the button's first baseline, and since the
   card actions row is baseline-aligned, the button then sat a pixel off from
   every other card's. `opacity: 0` hides it and keeps the box, so nothing
   shifts. The text is already handled by font-size: 0. */
li.product .add_to_cart_button.added > svg,
li.product .add_to_cart_button.added > img,
li.product .add_to_cart_button.added .ct-icon,
li.product .add_to_cart_button.added * {
	opacity: 0;
}

/* Keep the button's own empty pseudo-elements. Setting `content: none` removes
   a line box and moved the button up by a pixel — the empty ::after still takes
   part in layout. They are made invisible instead, which changes nothing. */
li.product .add_to_cart_button.added::before,
li.product .add_to_cart_button.added::after {
	content: '';
	opacity: 0;
	background-image: none;
}

/* ⑤ Breadcrumbs */
.woocommerce-breadcrumb,
.ct-breadcrumbs {
	font-size: var(--fs-sm);
	color: var(--kj-muted);
	margin-bottom: 22px;
}
.woocommerce-breadcrumb a, .ct-breadcrumbs a { color: var(--kj-muted); text-decoration: none; }
.woocommerce-breadcrumb a:hover, .ct-breadcrumbs a:hover { color: var(--kj-text); }

/* ⑥ Pagination: one geometry, current page in the highlight colour */
.woocommerce-pagination ul.page-numbers,
.page-links {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 6px;
	border-top: 0;
	padding-top: 0;
	margin-top: 32px;
	justify-content: center;
}
.woocommerce-pagination ul.page-numbers li { border: 0; }
.woocommerce-pagination .page-numbers a,
.woocommerce-pagination .page-numbers span,
.page-links a, .page-links > span {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 40px;
	height: 40px;
	padding: 0 12px;
	border: 1px solid var(--kj-border-strong);
	border-radius: var(--kj-radius-sm);
	color: var(--kj-text);
	text-decoration: none;
	transition: background-color 0.18s var(--kj-ease), border-color 0.18s var(--kj-ease);
}
.woocommerce-pagination .page-numbers a:hover,
.page-links a:hover { background-color: var(--kj-surface-2); border-color: var(--kj-muted); }
.woocommerce-pagination .page-numbers .current,
.page-links > span {
	background-color: var(--kj-surface-2);
	border-color: var(--kj-highlight);
	color: var(--kj-text);
}

/* ⑦ Tables (cart, order details, attributes): one header, one rule weight */
.shop_table,
.woocommerce-table {
	border: 1px solid var(--kj-border);
	border-radius: var(--kj-radius-sm);
	border-collapse: separate;
	border-spacing: 0;
	overflow: hidden;
}
.shop_table th, .woocommerce-table th {
	font-family: var(--kj-font-body);
	font-weight: 600;
	font-size: var(--fs-sm);
	letter-spacing: 0.02em;
	color: var(--kj-muted);
	background-color: var(--kj-surface);
	padding: 14px 16px;
}
.shop_table td, .woocommerce-table td { padding: 14px 16px; border-top: 1px solid var(--kj-border); }

/* ⑧ Empty states: icon, one line, one action — centred and calm */
.cart-empty, .woocommerce-info.cart-empty {
	max-width: 520px;
	margin: 48px auto;
	padding: 40px 24px;
	text-align: center;
	background-color: var(--kj-surface);
	border: 1px solid var(--kj-border);
	border-radius: var(--kj-radius);
	color: var(--kj-muted);
	font-size: var(--fs-md);
}
.return-to-shop { text-align: center; margin-bottom: 56px; }

/* ⑨ Product tabs */
.woocommerce-tabs ul.tabs { padding: 0; gap: 26px; border-bottom: 1px solid var(--kj-border); }
.woocommerce-tabs ul.tabs::before { border: 0; }
.woocommerce-tabs ul.tabs li { background: none; border: 0; border-radius: 0; margin: 0; padding: 0; }
.woocommerce-tabs ul.tabs li::before,
.woocommerce-tabs ul.tabs li::after { display: none; }
.woocommerce-tabs ul.tabs li a {
	display: block;
	padding: 0 0 12px;
	font-family: var(--kj-font-display);
	font-weight: 600;
	color: var(--kj-muted);
	text-decoration: none;
	border-bottom: 2px solid transparent;
	transition: color 0.18s var(--kj-ease), border-color 0.18s var(--kj-ease);
}
.woocommerce-tabs ul.tabs li a:hover { color: var(--kj-text); }
.woocommerce-tabs ul.tabs li.active a { color: var(--kj-text); border-bottom-color: var(--kj-highlight); }

/* ============================================================
   Notice banners — the box itself, not the layout
   ============================================================
   These are WooCommerce's info banners (customer-email-verification and the
   "no orders yet" prompt on the account pages). The order of things inside is
   left exactly as it was — icon, message, action on the right — only the frame
   is brought into the same system as the cards and buttons: token radius,
   token padding, a real border, and an icon scaled to the text instead of
   dwarfing it.

   Both the classic markup and the Blocks banner are covered, plus an
   attribute hook, because the banner renders through different templates in
   different contexts. */
.woocommerce-notice,
.woocommerce-info,
.woocommerce-message,
.woocommerce-error,
.wc-block-components-notice-banner,
[class*="notice-banner"] {
	display: flex;
	align-items: center;
	gap: 16px;
	padding: 16px 18px;
	border: 1px solid var(--kj-border);
	border-left-width: 3px;
	border-radius: var(--kj-radius);
	background-color: var(--kj-surface);
	color: var(--kj-text);
	font-size: var(--fs-base);
	line-height: 1.55;
}

/* The bell was noticeably larger than the sentence beside it. */
.woocommerce-notice > svg:first-of-type,
.woocommerce-info > svg:first-of-type,
.wc-block-components-notice-banner > svg:not([class*="dismiss"]),
[class*="notice-banner"] > svg:first-of-type {
	width: 18px;
	height: 18px;
	flex: 0 0 18px;
	opacity: 0.7;
}

/* The two banners sit at an even distance instead of via their own margins. */
.woocommerce-notices-wrapper {
	display: flex;
	flex-direction: column;
	gap: 10px;
}
.woocommerce-notices-wrapper > * { margin-top: 0; margin-bottom: 0; }

/* Narrow screens: the action drops to its own line rather than squeezing the
   message. Order and alignment are otherwise untouched. */
@media (max-width: 600px) {
	.woocommerce-notice,
	.woocommerce-info,
	.wc-block-components-notice-banner,
	[class*="notice-banner"] {
		flex-wrap: wrap;
		gap: 10px 14px;
	}
	.woocommerce-notice .button,
	.woocommerce-info .button,
	.wc-block-components-notice-banner .button,
	[class*="notice-banner"] .button { margin-left: 0; }
}




/* ============================================================
   Shop title band — figure like product-category heroes
   ============================================================
   The shop hero now gets hero-1.jpg as a type-2 figure (filter in
   functions.php), same markup as /product-category/keyboards/. A CSS
   background can never paint here: global.css sets background-image:none
   on [data-prefix="woo_categories"] with higher specificity than any
   body.shop selector. The scrim keeps the white H1 off the bright keys —
   photography behind type without one fails contrast no matter the photo. */
/* Every type-2 hero figure gets the same scrim — categories and shop share
   the markup, so they must share the contrast fix. 0.65 was the α that keeps
   the muted-looking subtitle (now brightened below) above 4.5:1 on the
   brightest keycap highlight we sampled (mice p90 lum 0.90). */
.hero-section[data-type="type-2"] > figure > .ct-media-container::after {
	content: "";
	position: absolute;
	inset: 0;
	background: linear-gradient(rgba(9, 9, 11, 0.65), rgba(9, 9, 11, 0.65));
}

/* Subtitle under archive H1s: brighten, brand font, and stop hiding it on
   phones (ct-hidden-sm was Blocksy's default mobile:false for descriptions). */
.hero-section .page-description,
.entry-header .page-description {
	font-family: var(--kj-font-body);
	color: var(--kj-text);
	font-weight: 600;
	font-size: 1.0625rem;
	line-height: 1.5;
}
.hero-section .page-description.ct-hidden-sm {
	display: block !important;
}


/* Meta under the title: dim token instead of alpha so audits see the real colour. */
.woocommerce span.sku,
.woocommerce .posted_in,
.woocommerce .posted_in a {
	opacity: 1;
	color: var(--kj-dim);
}
.woocommerce .posted_in a:hover { color: var(--kj-text); }


/* Unselected variation: stop reading as a live white CTA. Blocksy/WC still
   force opacity:.5 on .disabled, which dragged the label under 4.5:1. */
.single_add_to_cart_button.disabled,
.single_add_to_cart_button:disabled,
button.single_add_to_cart_button.disabled {
	background: var(--kj-border) !important;
	border-color: var(--kj-border-strong) !important;
	color: var(--kj-muted) !important;
	cursor: not-allowed;
	opacity: 1 !important;
}


.woocommerce-result-count {
	font-size: var(--fs-sm);
	color: var(--kj-muted);
}
