/*
 * PAJ Engraving configurator styles.
 *
 * SCOPING RULE: every selector in the first section is prefixed .paj-eng or is a
 * .paj-eng-* class. Zero bare element selectors. Flatsome applies uppercase,
 * letter-spacing, gradients and border-radius to `button` and `.button`
 * globally, and Dynamic Content for Elementor applies colours through
 * [data-dce-*] attribute selectors which can outrank a plain class. The reset
 * below neutralises that inside our root and nowhere else.
 *
 * REACTIVITY: the design recomputes twelve inline style objects on every colour
 * change. Here, per-chip values (--pf-*) are written ONCE server-side and never
 * touched, the live values (--paj-eng-*) are set by JS, and the static geometry
 * (--paj-geo-*) is owned by THIS FILE and only ever read — including by JS.
 * Three prefixes, three writers, no overlap.
 *
 * 🔴 WHERE the live defaults are declared is load-bearing, and getting it wrong
 * is a silent no-op. A custom property resolves per ELEMENT: a declaration ON
 * an element beats the value it would inherit from an ancestor. While these
 * defaults sat in the `.paj-eng` block below, .paj-eng carried its own
 * declaration of --paj-eng-ink, so every descendant read THAT and never the
 * value JS had written to :root. Selecting Bordeaux left all twelve chip
 * previews white, and the summary plate drew in the default face whichever font
 * was picked. They live on :root now, where nothing shadows the update.
 *
 * JS writes every live value TWICE, to documentElement and to the .paj-eng root
 * (see paintVars). The :root copy is the interface the out-of-root surfaces
 * (sticky bar, the gallery plate slide) read with no shared JS; the .paj-eng
 * copy is what overrides the style attribute the shortcode prints for first
 * paint, which is itself deliberate: per-product, and immune to CSS
 * aggregation.
 *
 * The last section styles nodes OUTSIDE our root. Those classes are still ours;
 * the elements they land on are not.
 */

/* =====================================================================
 * Tokens
 * ================================================================== */

/*
 * LIVE values, and the only block in this file that is not scoped to .paj-eng.
 *
 * On :root by necessity, not by preference: see the reactivity note in the file
 * header. A copy of any of these inside `.paj-eng` shadows the value JS writes
 * to documentElement and breaks colour and font reactivity outright, so do not
 * move them back, and do not add a second declaration for one of them anywhere
 * a .paj-eng ancestor could pick it up.
 *
 * The values here are what renders before JS runs, and what renders if JS never
 * runs at all. The shortcode also prints the product's configured defaults into
 * a style attribute on .paj-eng, which is what makes first paint show the right
 * face and ink rather than these generic ones.
 */
:root {
	--paj-eng-ink: #f2f2f0;
	/* ⚠ No CSS reads this since D22 removed the photo mark's relief, which was
	   its only consumer. Deliberately kept, not dead: engraving.js writes it on
	   every ink change, dev/ui-test-suite.js asserts that write, and it mirrors
	   the registry's hex_hi — an overlay-editable value sanitised in
	   class-registry-overlay.php. Retiring it is a registry change with a
	   registry_version cost, not a stylesheet tidy-up. */
	--paj-eng-ink-hi: #ffffff;
	/* Pre-JS first paint only: class-shortcode.php writes the registry's real
	   first face into the root's style attribute, and paintVars overwrites it
	   again. This is what shows if either is missing — an optimizer stripping the
	   attribute, or CSS applying before JS runs.

	   🔴 The weight must be a weight some face actually declares. It was 600, and
	   nothing in fonts.json is 600, so this default asked for a weight the
	   browser had to SYNTHESISE — a smeared fake bold over a generic cursive,
	   which is a rendering defect presenting as a font choice. The family was
	   'Caveat', a slug retired since 2026-08-06 and no longer in the Google URL,
	   so it resolved to whatever generic cursive the device had. Both corrected
	   0.9.19 to the current first picker face. If the picker order changes this
	   goes stale — harmlessly, but check it.

	   🔴 THE SUFFIX IS AN OPTION NUMBER, NOT A FACE, AND MUST STAY THAT WAY (D33).
	   This stylesheet is served to every visitor, so a real typeface name written
	   here — in a rule OR in a comment — hands back exactly what public_label
	   exists to withhold. Opt01 is the first picker face; look it up in
	   config/fonts.json if you need to know which one that is. */
	--paj-eng-fam: 'PajEngrave-Opt01', 'Great Vibes', cursive;
	--paj-eng-weight: 400;
	/* 🔴 NOW AN OPTICAL TRIM, AND NEUTRAL FOR ALL TWELVE FACES (0.9.22, D27).
	   Until 0.9.21 this multiplied a font-blind guess and had to absorb the whole
	   difference between one face and another; every value in fonts.json was
	   eyeballed against a GOOGLE SUBSTITUTE and never re-judged when 0.9.12 began
	   drawing the real woff2. The fit is measured on both axes now, so the
	   normalising is done by measurement and every preview_scale is 1.0. It stays
	   in the formulas, and stays in FONT_EDITABLE, so wp-admin can still nudge a
	   single face without a release. */
	--paj-eng-scale: 1;
	/* The placeholder "Buddy"'s length for the pre-JS first paint, NOT the
	   name cap. Change only if name_placeholder changes length. */
	--paj-eng-len: 5;
	/* The design's first-paint sizes, at the wide and narrow plates. Not
	   fitPlate() output — that is what JS publishes over the top of these. The
	   narrow one holds the plates' own 89/112 ratio. */
	--paj-eng-plate-size: 17px;
	--paj-eng-plate-size-narrow: 13.5px;
	--paj-eng-summary-size: 14px;
	--paj-eng-caps: none;
}

/*
 * STATIC geometry. Also on :root, and deliberately NOT named --paj-eng-*.
 *
 * The engraving window is ONE calibration consumed by TWO rules on two surfaces
 * — .paj-eng-card__engrave on the variant card and .paj-eng-plate__mark on the
 * gallery plate slide — and its width is also the boxW engraving.js needs for
 * the no-container-query fallback. Until 0.9.12 it was typed out three times:
 * once per rule here, and once in engraving.js as a bare `41`. Two of the three
 * copies were unreachable in a modern browser, so the JS one could rot in
 * silence; its own comment said so. There is one definition now, this one, and
 * everything else derives from it.
 *
 * UNITLESS on purpose. The same number has to serve both `left: …%` and the cqw
 * font-size formula, and CSS cannot graft a unit onto a var() — so each consumer
 * multiplies by the unit it needs: `* 1%`, `* 1cqw`, `* 1px`. Storing "36.6%"
 * would force a second declaration for the cqw form, i.e. exactly the mirror
 * this block exists to delete.
 *
 * --paj-geo-*, never --paj-eng-*: see the reactivity note in the file header.
 * A --paj-eng-* declaration anywhere a .paj-eng ancestor can see it shadows the
 * value JS writes to documentElement, so the two namespaces must stay
 * distinguishable at a glance. JS READS these (see plateBoxW in engraving.js)
 * and must never write them; dev/ui-test-suite.js asserts both halves.
 */
:root {
	/* The two widths of .paj-eng-card__plate, in px: the reference plate and the
	   narrow one used below the 420cqw / 600px breakpoint.

	   Both are tokens since 0.9.19, and the narrow one is no longer a literal.
	   It was, deliberately, on the reasoning that "the JS fallback applies at
	   EVERY width and is calibrated against this one" — which stated the bug
	   rather than a design. The fallback DOES apply at every width, so being
	   calibrated against 112 made it wrong at 89: a name sized for the wide
	   window overflowed the narrow one by 26% inside overflow:hidden, on the
	   old-Safari path, on phones only. engraving.js now derives a size from each
	   (plateBoxW), and the @media rule inside @supports picks. */
	--paj-geo-plate-w: 112;
	--paj-geo-plate-w-narrow: 89;

	/* The font chip's outer width in px, and the 2px border it carries, so the
	   content box the preview has to fit inside is derivable rather than typed.
	   fitChips() in engraving.js reads this instead of the bare 78 it used to
	   carry as a literal — the constant its own comment warned "goes stale
	   silently" if this width moves. One definition, three readers: the width
	   declaration below, the width cap in .paj-eng-chip__preview, and the JS. */
	--paj-geo-chip-w: 80;
	--paj-geo-chip-border: 2;

	/* The chip's outer height, the vertical twin of the width above and the box
	   the chip's own size formula divides by. A literal 40px until 0.9.22, when
	   the chip stopped being sized by a nominal 16px and started being FITTED to
	   its box like every other surface — at which point its height became load
	   bearing and had to stop being typed in one place and assumed in another.
	   Mirrored by the `height` declaration on .paj-eng-chip. */
	--paj-geo-chip-h: 40;

	/* 🔴 How wide one character is, in em — the horizontal twin of
	   --paj-eng-scale, and the number D26 exists because nobody had.

	   0.52 is a FALLBACK, not a design value: it is only right for a normal-width
	   text face. The real per-face figures are measured with fontTools into
	   fonts.json preview_advance_em and published here by class-shortcode.php
	   (pre-JS) and paintVars() (per keystroke, from a canvas measurement of the
	   string actually on screen in the face actually drawn).

	   PajEngrave-Opt01 sets 0.758. Every formula below that multiplies
	   --paj-eng-len by this used to hardcode 0.52, so a five-letter name in that
	   face was sized 46% too large for its window and overflow:hidden took both
	   ends off it. See fonts.json _measured_advance_note.

	   🔴 RAISED 0.52 -> 0.62 IN 0.9.22, and the direction is the whole point.
	   Until then this sat in a HEURISTIC arm that was capped by a separate
	   geometric one, so under-estimating it merely wasted room. It is now the
	   DIVISOR of the arm that decides the size, and under-estimating a divisor
	   makes the type too big — i.e. clipped. 0.62 is above every text face
	   measured for the four that carry no figure of their own (Times New Roman
	   Bold 0.582, Arial Bold 0.603, Impact 0.481, Courier New 0.600, Verdana
	   0.615), so the fallback now errs small-and-safe rather than large-and-cut.
	   A face with a real measurement never sees it. */
	--paj-eng-adv: 0.62;

	/* 🔴 THE VERTICAL TWIN, NEW IN 0.9.22 (D27), and the number that lets a name
	   grow without being cut. How tall the face's ink is, in em, measured as the
	   box it actually needs rather than the ink it contains: align-items:center
	   centres the 1em LINE BOX and a script's ink is not symmetric about it, so
	   the box must clear TWICE THE LARGER SIDE. PajEngrave-Opt01 inks 1.860em
	   and needs 2.040em.

	   Measured into fonts.json preview_box_em by dev/measure-faces.py, published
	   here by class-shortcode.php, and republished per keystroke by paintVars()
	   from a canvas measurement of the string actually on screen.

	   1.35 is the fallback for a face nobody has measured, chosen the same way
	   and in the same direction as --paj-eng-adv above: it is above every OS text
	   face measured (1.066 to 1.328), and over-estimating draws smaller. */
	--paj-eng-box: 1.35;

	/* Fraction of a window the fitted name may occupy. Mirrored in
	   engraving.js (ADV_MARGIN) and class-registry.php (ADVANCE_MARGIN).
	   Not 1: ink on the clip edge stops reading as engraved, and every one of
	   these windows sits on a rounded corner or a photographed housing. */
	--paj-eng-adv-margin: 0.94;

	/* The same fraction for the font CHIPS, which are tighter than a plate: a
	   chip is a control with a visible 1px border and an 8px radius, so ink at
	   0.94 of its box touches the corner curve. 0.88 leaves ~4.7px each side of
	   the 78px content box — the same breathing room fitChips() used to subtract
	   as a bare `- 4`, expressed as a fraction so it tracks the chip size. */
	--paj-eng-chip-margin: 0.88;

	/* 🔴 Read the note over .paj-eng-card__engrave before touching these four.
	   left/width and the vertical CENTRE are calibration against the render;
	   height is only a clip box, and may grow about that centre or not at all. */
	--paj-geo-window-l: 26.7;
	--paj-geo-window-t: 28.3;
	--paj-geo-window-w: 46.6;
	--paj-geo-window-h: 45.4;

	/* The plate's own height as a percentage of its own width, i.e. the 290x196
	   render's aspect (196/290 = 67.586). The ONLY way to express the clip box's
	   HEIGHT in the same cqw the width formulas use: cqw is a share of the
	   container's width, and --paj-geo-window-h is a percentage of its height, so
	   converting one to the other needs the aspect and nothing else.

	   Mirrors `aspect-ratio: 290 / 196` on .paj-eng-card__plate and on
	   .paj-eng-plate. If a future render changes shape, all three move together
	   or the vertical arm silently sizes against the wrong box. */
	--paj-geo-plate-aspect: 67.586;
}

.paj-eng {
	--paj-eng-orange: #f05123;
	--paj-eng-navy: #1a2839;
	--paj-eng-cta: #f87d3b;
	--paj-eng-link: #40638c;
	--paj-eng-muted: #64748b;
	--paj-eng-disabled: #94a3b8;
	--paj-eng-border: #cbd5e1;
	--paj-eng-surface: #f0f5fa;
	--paj-eng-amber-bg: #fdebd2;
	--paj-eng-amber-fg: #8a4a12;
	--paj-eng-plate: #17181c;
	--paj-eng-radius: 12px;
	--paj-eng-radius-sm: 8px;

	/* NO live values here. They are on :root above, and a copy in this block is
	   what broke colour and font reactivity. */

	box-sizing: border-box;
	font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
	color: var(--paj-eng-navy);
	line-height: 1.45;
	-webkit-font-smoothing: antialiased;
}

.paj-eng *,
.paj-eng *::before,
.paj-eng *::after {
	box-sizing: border-box;
}

/*
 * Theme reset, scoped.
 *
 * ⚠️ SPECIFICITY: this selector is (0,1,1), one class plus one element, which
 * outranks a plain .paj-eng-chip at (0,1,0). It has to be that strong to beat
 * Flatsome's and Elementor's own `button` rules. The consequence is that every
 * component rule below which sets a property this reset touches (background,
 * border, border-radius, padding, colour) MUST be written as
 * `.paj-eng .paj-eng-thing` (0,2,0) rather than `.paj-eng-thing`.
 *
 * This is not stylistic. Getting it wrong renders the font chips and colour
 * swatches with no background and no border, meaning white text on white and
 * fifteen invisible circles, which is exactly what happened the first time.
 */
.paj-eng button {
	margin: 0;
	padding: 0;
	background: none;
	border: 0;
	font: inherit;
	color: inherit;
	text-transform: none;
	letter-spacing: normal;
	box-shadow: none;
	border-radius: 0;
	line-height: normal;
	min-height: 0;
	text-shadow: none;
	outline: none;
}

/*
 * NO FOCUS OUTLINE ON ANY BUTTON IN THE CONFIGURATOR.
 *
 * Requested by PAJ on 2026-08-06, after the ring around "Weniger Schriften
 * anzeigen". Both pseudo-classes are listed on purpose: :focus-visible alone
 * would not cover the browsers that ring a button on a plain mouse click, which
 * is the case that prompted the request.
 *
 * 🔴 What this costs, recorded rather than argued: a keyboard user now has no
 * indication of which of the twelve font chips or sixteen swatches is focused.
 * The roving tabindex still makes each row one tab stop and arrow keys still
 * move focus, but nothing on screen says where focus is. This widget's own notes
 * flag BFSG as applying to this shop from June 2025. A middle option exists and
 * was offered: ring on keyboard, none on mouse, via :focus:not(:focus-visible).
 * PAJ chose full removal.
 *
 * NOTE what is deliberately NOT here: box-shadow. The rings on a selected chip
 * or swatch are SELECTION cues driven by aria-checked, and a :focus rule setting
 * box-shadow:none would outrank them at (0,3,1) and erase the selected state on
 * the very element the customer just clicked.
 */
.paj-eng button:focus,
.paj-eng button:focus-visible {
	outline: none;
}

.paj-eng input[type='text'] {
	margin: 0;
	box-shadow: none;
	background: #fff;
	min-height: 0;
}

.paj-eng p {
	margin: 0;
}

/* Kept for the name INPUT and the spelling checkbox, which are not buttons and
   are not covered by the removal above. The spelling checkbox in particular is
   a gate the CTA sends the customer to, so it has to be findable when it
   receives focus programmatically. */
.paj-eng input:focus-visible {
	outline: 2px solid var(--paj-eng-link);
	outline-offset: 2px;
}

/*
 * The [hidden] attribute only carries `display:none` from the UA stylesheet, so
 * ANY component rule setting display (flex, grid, block) silently defeats it.
 * JS toggles `hidden` on the summary, price, spell row, notes, hint and caps
 * note, so one blanket rule is worth more than six per-component ones, and it
 * covers whatever gets added next.
 */
.paj-eng [hidden] {
	display: none !important;
}

.paj-eng-sr {
	position: absolute !important;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

/* =====================================================================
 * Root layout
 * ================================================================== */

.paj-eng {
	display: unset;
	width: 100%;
	container-type: inline-size;
}

.paj-eng-notices:empty {
	display: none;
}

/* =====================================================================
 * Variant cards
 * ================================================================== */

.paj-eng-cards {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: 12px;
}

.paj-eng .paj-eng-card {
	position: relative;
	display: block;
	width: 100%;
	text-align: center;
	background: #fff;
	cursor: pointer;
	border-radius: var(--paj-eng-radius) !important;
	transition: border-color 200ms, box-shadow 200ms;

	/* Unselected carries a 1px border and +1px padding on every side. That extra
	   pixel is not a typo: it compensates for the thinner border so the card
	   does not shift by 1px when selection moves. Straight from the design. */
	border: 1px solid var(--paj-eng-border);
	padding: 17px 13px 15px;
}

.paj-eng .paj-eng-card[aria-checked='true'] {
	border: 2px solid var(--paj-eng-orange);
	padding: 16px 12px 14px;
	box-shadow: 0 4px 12px rgba(240, 81, 35, 0.12);
}

.paj-eng-card__badge {
	position: absolute;
	top: -11px;
	left: 50%;
	transform: translateX(-50%);
	background: var(--paj-eng-orange);
	color: #fff;
	border-radius: 999px;
	font-size: 11px;
	font-weight: 700;
	padding: 3px 11px;
	letter-spacing: 0.5px;
	white-space: nowrap;
}

.paj-eng-card__tick {
	position: absolute;
	top: 10px;
	right: 10px;
	width: 20px;
	height: 20px;
	border-radius: 50%;
	background: var(--paj-eng-orange);
	color: #fff;
	font-size: 13px;
	line-height: 1;
	display: none;
	align-items: center;
	justify-content: center;
}

/* The tick is the non-colour selection cue, so it must not be the only one:
   the 1px→2px border change carries it too, for anyone who cannot distinguish
   the orange. */
.paj-eng-card[aria-checked='true'] .paj-eng-card__tick {
	display: flex;
}

.paj-eng-card__plate {
	display: flex;
	align-items: center;
	justify-content: center;
	/* 112px, not 96: the 0.9.7 render pair frames the device more loosely than
	   housing.png did. The device fills 68.6% of the frame (199px of 290) where
	   housing.png filled 96.9%, so 112 × 0.686 = 77px — the size the standard
	   card already drew. The named card comes DOWN to meet it; at 96px both
	   cards would have shrunk to 66px instead.

	   The 112 itself lives in the --paj-geo-* token block, because engraving.js
	   multiplies it by the window width to derive its fallback boxW. */
	width: calc(var(--paj-geo-plate-w) * 1px);
	margin: 0 auto 12px;
	position: relative;

	/* Both cards show a 290×196 render since 0.9.7, so the two card titles line
	   up by construction. This previously pinned the box to the named card's
	   293×256 housing.png and centred the flatter standard render inside it. */
	aspect-ratio: 290 / 196;

	/* cqw in __engrave resolves against this box, the .paj-eng-plate pattern:
	   1cqw = 1.12px here, 0.89px at the narrow block's 89px, so the on-device
	   text scales with the plate instead of overflowing a smaller window. */
	container-type: inline-size;
}

.paj-eng-card__housing {
	width: 100%;
	height: auto;
	display: block;
	pointer-events: none;
}

/* 0.9.7 deleted a `transform: scale(1.17)` that lived here on the standard
   card's render. Do not put a scale correction back. It existed because the two
   cards used differently-cropped renders, and it was derived from a bbox of
   237/290 (81.7%) for hardware-led.webp — a figure that included the soft
   shadow. The opaque device is 199/290 (68.6%), so the correction under-scaled
   and left the cards 21% apart: 77px against the named card's 93px. That
   mismatch is what PAJ reported. Both cards now draw the SAME 290×196 render
   pair, geometrically identical bar the printed wordmark, so there is nothing
   left to correct. */

/* The engraving window on the housing render: 26.7% offset, 46.6% wide, centred
   on 51.0% of the height, over the 290×196 render pair (0.9.7). Those numbers
   are NOT written below — they are read from --paj-geo-window-* on :root, the
   one place they are defined. Recalibrate there and the plate slide and the JS
   fallback follow; there is nothing left to keep in step by hand. Only the named
   card overlays text since 0.8.1 — the standard card's render ships with the
   wordmark already printed, so its clipping-prone CSS twin of the logo is gone
   rather than patched.

   🔴 WIDENED 36.6% -> 46.6% IN 0.9.22 (D27), ABOUT THE SAME CENTRE: left moves
   31.7 -> 26.7 so 26.7 + 23.3 = 50.0% exactly as 31.7 + 18.3 did. The mark does
   not move; it is allowed to run further.

   The old width was calibrated against the PRINTED LOGO, and that was the error.
   Diffing the two render twins isolates the printed wordmark at x 96..192 of
   290, and the old window spanned x 92..198 — the logo's slot plus ~4px each
   side. But the engravable METAL PLATE spans x 65..224, and a laser is not
   confined to where marketing printed a logo. PAJ's own reference sheet
   (farb-schrift-uebersicht-a-1.webp — NO LONGER IN THE BUILD since 0.9.34, which
   deleted both retired trios; recover the exact pixels these numbers came from with
   `git show 0.9.33:paj-engraving/assets/ref/farb-schrift-uebersicht-a-1.webp`. The
   letter is named rather than updated to -c because -a is what was measured, and -b
   and -c only relabel the same tiles) draws the name at 46.3%
   to 77.0% of the tracker's width, median 63.8%, and the photograph of twelve real
   engraved trackers agrees; the old window could not exceed 50.1% however
   perfectly a face filled it. 46.6% of the render is 67.0% of the device, so a
   name at the margin lands on 63.0% — the sheet's median — and still stops
   11px short of the plate edge at each side. Verified: 46.6% spans x 77..213
   inside the plate's 65..224.

   🔴 THE FOUR --paj-geo-window-* NUMBERS ARE TWO DIFFERENT JOBS. left/width and the vertical
   CENTRE are calibration against the render — against the PLATE, as of 0.9.22,
   not against the printed logo — and they decide where the mark sits and how far
   it may run. height is only a CLIP BOX, and it is sized for the tallest FACE,
   not for the printed window: undersized it slices the flourish and descender
   flat (the same em-box cut as the font chips), which is what 0.8.x fixed here
   and what 0.9.19 had to fix again. Grow it about its centre or not at all.

   🔴 SIZE IT AGAINST THE FACE THAT IS DRAWN, NOT ITS SUBSTITUTE (D23). That is
   how this broke twice. 0.9.3 measured the box against Great Vibes (1.5em) and
   Pacifico (1.76em) — the Google substitutes. 0.9.12 then switched
   font_delivery to "self" and started drawing PajEngrave-Opt01 and
   PajEngrave-Opt03 instead, silently invalidating the calibration: nothing
   in the build reads a font file, so nothing could notice. The per-face ink
   boxes are no longer copied into this comment to go stale a third time: they
   are fonts.json preview_box_em, dev/measure-faces.py measures them off the
   shipped woff2 and exits non-zero when the file and the registry disagree.

   45.4% is the metal plate itself. The render's plate spans y 55..144 of 196 =
   45.408%, so the clip box is exactly the engravable area, and a glyph is cut
   only where the laser would run off the plate. top 28.3% keeps the centre on
   51.0% (28.3 + 22.7 = 51.0). Previously 36.4% / top 32.8%.

   🔴 0.9.22 INVERTED WHAT THIS NUMBER IS FOR, and that is worth understanding
   before touching it. Until now the box was a CONSEQUENCE — sized big enough for
   whatever the tallest face happened to need at whatever size the font-blind
   formula happened to pick, which is why it had to be re-derived every time
   either changed, and why it was wrong twice. It is now an INPUT: the vertical
   arm of the size formula divides this box by the face's measured
   --paj-eng-box, so the type is sized to fit the plate instead of the plate
   being resized to fit the type. Growing it makes short names bigger; shrinking
   it makes them smaller; neither can clip, because no face is ever drawn larger
   than the box divided by its own ink height.

   Typography is the hardened tier + !important: the same hostile page CSS
   that flattened the font chips rendered this span in the wrong face. */
.paj-eng .paj-eng-card__engrave {
	position: absolute;
	left: calc(var(--paj-geo-window-l) * 1%);
	top: calc(var(--paj-geo-window-t) * 1%);
	width: calc(var(--paj-geo-window-w) * 1%);
	height: calc(var(--paj-geo-window-h) * 1%);
	display: flex;
	align-items: center;
	justify-content: center;
	white-space: nowrap;
	overflow: hidden;

	/* No text-shadow. 🔴 D22: the engraved-letter relief is GONE from every
	   surface — do not reintroduce it here or on the five siblings listed in
	   that decision. The pairing this used to carry (0 1px black below, 0 -1px
	   10% white above) is a hard 1px offset with zero blur, and this box renders
	   the name at roughly 12-17px, so each offset was a second copy of the glyph
	   one pixel away. On the two hairline scripts that reads as an outline, which
	   is what PAJ reported: "a shadow or a border on the words". D14 kept these
	   deliberately and D22 overrides that half of it. */
	font-family: var(--paj-eng-fam) !important;
	font-weight: var(--paj-eng-weight) !important;
	color: var(--paj-eng-ink);
	text-transform: var(--paj-eng-caps, none) !important;
	line-height: 1 !important;

	/* fitPlate() from engraving.js transcribed to cqw, the same formula as
	   .paj-eng-plate__mark further down: boxW = the window width, in cqw of the
	   plate, so one token drives both the box and the type inside it. At the
	   112px plate that is 52.19px; engraving.js derives the same product for its
	   fallback (see plateBoxW). At the 89px narrow plate the text scales down
	   instead of keeping its 112px-sized pixel value.

	   🔴 0.9.22 MADE THE GEOMETRIC ARM THE DRIVER AND GAVE IT A VERTICAL TWIN
	   (D27). 0.9.21 added the horizontal arm as a CAP over the design's own
	   heuristic — a character count times a fixed 0.52 em, ceilinged at 30% of
	   the window — and this comment then argued that turning the cap into a
	   replacement "is arguably more correct and is emphatically not what was
	   reported". It is what was reported now. PAJ, with the configurator on
	   opt-09 and a photograph of twelve real engraved trackers: "they should be
	   big like this without cut it".

	   The heuristic could only ever draw a face SMALLER than its window allows,
	   and the narrower the face the smaller it drew it, because 0.52 em per
	   character was a guess about width that the min() was never allowed to
	   revise upwards. opt-09 sets 0.342, so its name filled 56% of a window it
	   could have filled whole; opt-02 0.416, 64%. Measured against PAJ's own
	   reference sheet every face bar opt-01 was between 1.10x and 1.79x
	   too small.

	   Both arms are measurements now, and between them they are the entire
	   statement: this much ink, this wide a window, this tall a box, therefore
	   exactly this large.

	     first arm   fill the WIDTH. --paj-eng-adv is the advance per character,
	                 from fonts.json before JS and from a canvas measurement of
	                 the string actually drawn afterwards (paintVars).
	     second arm  fill the HEIGHT — and this is the arm that stops a short name
	                 being cut. --paj-eng-box is the ink box the face needs,
	                 measured the same two ways. The 30%-of-window ceiling it
	                 replaces was doing this job in the wrong units and by eye: a
	                 WIDTH that happened to keep most faces inside a HEIGHT.

	   --paj-geo-window-h is a percentage of the plate's HEIGHT while cqw is a
	   share of its WIDTH, which is what --paj-geo-plate-aspect converts between.

	   🔴 STILL A min(), and now for a stronger reason than in 0.9.21: each arm is
	   a real box the ink has to fit inside, so the smaller one is the answer and
	   neither may be dropped. Without the second arm a 2-letter name in opt-09
	   lays 56px of ink into a 34px box. */
	font-size: calc(min(
		(var(--paj-geo-window-w) * 1cqw) * var(--paj-eng-adv-margin, 0.94) / (var(--paj-eng-len, 5) * var(--paj-eng-adv, 0.62)),
		(var(--paj-geo-window-h) * var(--paj-geo-plate-aspect) / 100 * 1cqw) * var(--paj-eng-adv-margin, 0.94) / var(--paj-eng-box, 1.35)
	) * var(--paj-eng-scale, 1)) !important;
}

.paj-eng-card__title {
	display: block;
	font-weight: 700;
	font-size: 16px;
}

.paj-eng .paj-eng-card__sub {
	display: block;
	font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif !important;
	font-size: 13px !important;
	font-weight: 400 !important;
	line-height: 1.45 !important;
	color: var(--paj-eng-muted);
	margin-top: 2px;
}

/* wc_price() emits .woocommerce-Price-amount > bdi > …currencySymbol inside
   the named card's sub, and Woo/theme/kit rules style those classes globally
   — on the draft review page the amount rendered in theme typography. The
   design: muted label, bold navy amount, one typeface, one size (0.8.1). */
.paj-eng .paj-eng-card__sub .woocommerce-Price-amount,
.paj-eng .paj-eng-card__sub .woocommerce-Price-amount bdi,
.paj-eng .paj-eng-card__sub .woocommerce-Price-currencySymbol {
	font-family: inherit !important;
	font-size: inherit !important;
	font-weight: 700 !important;
	color: var(--paj-eng-navy) !important;
}

/* =====================================================================
 * Expanding panel
 * ================================================================== */

.paj-eng-panel {
	max-height: 0;
	opacity: 0;
	overflow: hidden;
	transition: max-height 300ms ease-out, opacity 300ms ease-out;
}

.paj-eng-panel.is-open {
	/* Ceiling, not a height. 1200px → 1240px when the 2026-08-10 spacing pass
	   added ~20px to the fully-revealed panel; overflow:hidden below would clip
	   a too-small value silently. */
	max-height: 1240px;
	opacity: 1;
}

/* overflow:hidden is required for the max-height transition, and it clips the
   4px selection ring on the outermost chips. The design does not account for
   this. 6px of inner padding gives the ring room inside the clip. */
.paj-eng-panel__inner {
	padding: 6px;
	margin-top: 16px;
	display: flex;
	flex-direction: column;
	gap: 16px;
}

/* 1.3.4: a CLOSED panel takes no part in layout. It is max-height:0 and clipped, but
   without this the browser still lays the twelve chips out and fetches the faces
   they use (Opt02 alone is 235 KB) on every pageview, for a panel almost nobody
   opens. content-visibility:auto was tried first and does not work for this: Chrome
   counts the clipped, zero-height box as touching the viewport margin and unlocks it
   as soon as the main thread is free. display:none skips layout in every browser.
   The discrete transition keeps display:flex for the 300ms of the closing animation
   where it is supported (Chrome 117+, Safari 17.4+, Firefox 129+); elsewhere a
   closing panel simply vanishes at once. Opening is immediate everywhere. The faces
   are requested on intent instead: see "Preview faces" in engraving.js. Not nested
   in :where()/:is(), which would lower the specificity. */
.paj-eng-panel__inner {
	transition: display 300ms allow-discrete;
}

.paj-eng .paj-eng-panel:not(.is-open) > .paj-eng-panel__inner {
	display: none;
}

/* =====================================================================
 * Name field
 * ================================================================== */

.paj-eng .paj-eng-label {
	font-size: 14px;
	font-weight: 500;
	display: block;
	margin-bottom: 6px;
}

.paj-eng .paj-eng-inputwrap {
	position: relative;
}

/*
 * !important on every visually-defining property, not just the reset-touched
 * ones: Elementor's kit styles fields as
 * `.elementor-kit-N input:not([type=button]):not([type=submit])` at (0,3,1),
 * which no class chain of ours can outrank. That kit rule is what flattened
 * this field to a thin square box on the live PDP (Johnny, 2026-08-10) while
 * every !important-ed component around it kept its radius.
 */
.paj-eng .paj-eng-input {
	width: 100%;
	height: 64px !important;
	border: 1px solid var(--paj-eng-border) !important;
	border-radius: var(--paj-eng-radius) !important;
	padding: 0 64px 0 20px !important;
	font-size: 24px !important;
	font-family: inherit;
	color: var(--paj-eng-navy) !important;
	background: #fff !important;
	transition: border-color 150ms, background-color 300ms;
}

/* border-color must stay !important below: the base border above is
   !important, and an important shorthand beats a plain longhand even from a
   more specific rule. */
.paj-eng .paj-eng-input:hover {
	border-color: #94a3b8 !important;
}

/* Flashed for 800ms when the client removes a character the server would also
   have refused. The plugin's rule is reject loudly, never strip silently, and the
   visual flash plus the aria-live announcement together are the "loudly". */
.paj-eng .paj-eng-input.is-rejected {
	border-color: var(--paj-eng-orange) !important;
	background: #fde7df !important;
}

.paj-eng .paj-eng-counter {
	position: absolute;
	right: 16px;
	bottom: 10px;
	font-size: 13px;
	font-weight: 500;
	transition: color 150ms;

	/* The design uses #94A3B8, which is 2.8:1 on white and fails WCAG AA for
	   text. BFSG applies to this shop from June 2025, so: #64748B, 4.6:1. */
	color: var(--paj-eng-muted);
}

.paj-eng .paj-eng-counter.is-warn {
	color: var(--paj-eng-orange);
}

/* =====================================================================
 * Row labels ("Schriftart: Adobe Handwriting")
 * ================================================================== */

.paj-eng-rowlabel {
	font-size: 14px;
	font-weight: 500;
	margin: 0 0 8px !important;
}

.paj-eng-rowlabel__value {
	font-weight: 400;
	color: var(--paj-eng-muted);
}

/* "Farbe:" starts a new group and needs air above it — the reveal link's 8px
   bottom padding alone left the two rows visually welded (design review,
   2026-08-10). :not(:first-child) keeps "Schriftart:", the style block's first
   row, governed by the panel's own 16px gap instead of stacking onto it.
   !important because the base rule's margin shorthand above is !important. */
.paj-eng-style .paj-eng-rowlabel:not(:first-child) {
	margin-top: 12px !important;
}

/* =====================================================================
 * Font chips
 * ================================================================== */

.paj-eng-fontrow {
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
	align-items: center;
}

/* display:contents keeps the radiogroup in the accessibility tree while
   removing its box, so the chips wrap as part of the parent row. */
.paj-eng-fonts {
	display: contents;
}

/*
 * 🔴 80px minus the two 1px borders is the 78px content box that fitChips()
 * in engraving.js measures against. Move this width and the measured fit goes
 * stale silently — the chip would simply start clipping again. The constant
 * there is a fallback for hidden chips only; the live value is read off this
 * box, so the two stay in step as long as nobody hardcodes a third number.
 *
 * 0.9.21: both numbers are --paj-geo-* tokens now, so "nobody hardcodes a third"
 * is enforced rather than requested. engraving.js reads the tokens for its
 * hidden-chip fallback, and the width cap in .paj-eng-chip__preview derives the
 * same content box, so this declaration is the single definition of all three.
 *
 * 0.9.22: the HEIGHT joined them, for the same reason one version later. The
 * chip is fitted to its box on both axes now, so 40px stopped being decoration
 * and became the divisor of the vertical arm.
 */
.paj-eng .paj-eng-chip {
	position: relative;
	width: calc(var(--paj-geo-chip-w) * 1px);
	height: calc(var(--paj-geo-chip-h) * 1px);
	border-radius: var(--paj-eng-radius-sm) !important;
	background: var(--paj-eng-plate);
	cursor: pointer;
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: hidden;
	flex: none;
	transition: box-shadow 200ms, border-color 200ms;
	border: 1px solid var(--paj-eng-border);
}

/* Driving the visual off aria-checked rather than a class makes "looks selected
   but is not announced as selected" structurally impossible. */
.paj-eng .paj-eng-chip[aria-checked='true'] {
	border-color: transparent;
	box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--paj-eng-orange);
}

/*
 * Hardened like the name input (0.7.1 D2): on the draft review page a
 * site-wide rule flattened all twelve chips to one face at one size while
 * the inline --pf-* attributes and the data-caps transform kept working, so
 * whatever matched the span outranked a bare (0,1,0) class. Same cure as
 * the input: our tier plus !important on every typographic property. No
 * container/media block re-declares any of these, so the container-query
 * caveat further down does not apply here.
 */
.paj-eng .paj-eng-chip__preview {
	font-family: var(--pf-fam) !important;
	font-weight: var(--pf-weight) !important;

	/*
	 * 🔴 THE SAME TWO-AXIS FIT AS THE CARD WINDOW (0.9.22, D27) — read the note
	 * over .paj-eng-card__engrave first; this is that formula against the chip's
	 * own box instead of the plate's, and the two must change together.
	 *
	 * What it replaces: the design's `16px * scale * min(1, 7/nameLen)`, capped
	 * since 0.9.21 by a measured width. That base was a NOMINAL size — every chip
	 * at 16px whatever face it drew — so a chip showed a narrow face small and a
	 * wide face large, which is not what a picker is for. The customer is asking
	 * "how will my name look in this one", and the honest answer is each face at
	 * the largest size its own chip can hold. PAJ reported the symptom on opt-09,
	 * which at 0.342 em per character filled 49% of its chip while
	 * opt-01 filled 94% of the identical box.
	 *
	 * --pf-adv and --pf-box are the per-chip twins of --paj-eng-adv/-box: printed
	 * inline per chip by class-shortcode.php from the registry, then overwritten
	 * by fitChips() with a canvas measurement of the string that chip actually
	 * paints. Per chip and not per root because every chip draws a DIFFERENT face
	 * at the same moment, and because a caps chip paints capitals — on
	 * opt-01 capitals and lowercase differ by 73%, so the registry's mean
	 * would shrink eight lowercase letters by a third to pay for capitals that
	 * are not on screen.
	 *
	 * 🔴 NEITHER ARM MAY BE DROPPED, and the fallbacks are deliberately the
	 * pessimistic ones (0.62 em wide, 1.35 em tall). Where 0.9.21's min() could
	 * only shrink a chip, both arms now decide its size, so a fallback that
	 * UNDER-states a face draws it too big and clips it. fitChips() writes the
	 * registry's static figure rather than removing the property when it cannot
	 * measure, for exactly this reason.
	 *
	 * --paj-eng-chip-margin, not --paj-eng-adv-margin: a chip is a bordered
	 * control with an 8px radius, so it needs more clearance than a plate.
	 */
	font-size: calc(min(
		(var(--paj-geo-chip-w) - var(--paj-geo-chip-border)) * 1px * var(--paj-eng-chip-margin, 0.88) / (var(--paj-eng-len, 5) * var(--pf-adv, 0.62)),
		(var(--paj-geo-chip-h) - var(--paj-geo-chip-border)) * 1px * var(--paj-eng-chip-margin, 0.88) / var(--pf-box, 1.35)
	) * var(--pf-scale, 1)) !important;
	color: var(--paj-eng-ink);
	/* No text-shadow — D22, see .paj-eng-card__engrave. */
	white-space: nowrap;
	line-height: 1 !important;
	max-width: 100%;

	/*
	 * 🔴 visible, and it must stay visible. THE CHIP IS THE CLIPPER; THIS SPAN
	 * IS NOT. With line-height:1 above, this box is exactly one em tall, so
	 * overflow:hidden here clipped at the em box — and the script faces do not
	 * fit inside their em box. PajEngrave-Opt01 (scale 1.35) sets at 21.6px
	 * and inks 1.86em ≈ 40px; PajEngrave-Opt03 1.556em. Both lost their
	 * ascender flourish and their descender to a flat cut, while the 38px chip
	 * around them had the room all along.
	 *
	 * Headroom, re-measured in 0.9.19 against the self-hosted faces the shop
	 * actually draws (0.9.3 measured the Google substitutes, which is how the
	 * card's window silently went stale — see the note over
	 * .paj-eng-card__engrave). Opt01 is the tightest: 17.9px above the line-box
	 * centre for a German name against the chip's 19px half-height. It fits, and
	 * the 38px chip is deliberately NOT resized. It does graze for the
	 * Latin-Extended accents CHARSET_RE also permits (Ĺ at 22.0px), which is
	 * accepted — a chip is a face sample, not the proof, and widening it would
	 * relayout the whole picker to spare one glyph nobody engraves.
	 *
	 * Nothing moves sideways: max-width:100% keeps this box at the chip's own
	 * 78px content width, so .paj-eng-chip's overflow:hidden clips a long name
	 * at the very same edge it did before. Only the vertical clip changes,
	 * 21.6px -> 38px.
	 */
	overflow: visible;
}

/* Each chip previews its OWN face, so the caps rule keys off that chip's
   server-printed data-caps, NOT off --paj-eng-caps, which tracks the SELECTED
   face. Using the variable here would uppercase all twelve chips the moment a
   caps-only face was picked. */
.paj-eng .paj-eng-chip[data-caps='1'] .paj-eng-chip__preview {
	text-transform: uppercase !important;
}

/* =====================================================================
 * Colour swatches
 * ================================================================== */

.paj-eng-colourrow {
	display: flex;
	flex-wrap: wrap;
	gap: 10px;
	align-items: center;
}

.paj-eng-colours {
	display: contents;
}

.paj-eng .paj-eng-swatch {
	position: relative;
	width: 36px;
	height: 36px;
	border-radius: 50% !important;
	background: var(--pf-ink);
	cursor: pointer;
	display: flex;
	align-items: center;
	justify-content: center;
	flex: none;
	transition: box-shadow 200ms;

	/* The border is not decoration: the white swatch is invisible on a white
	   card without it. */
	border: 1px solid var(--paj-eng-border);
}

/* Navy ring here, orange on the font chips, deliberately different in the
   design, so the two rows never read as one selection. */
.paj-eng .paj-eng-swatch[aria-checked='true'] {
	box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--paj-eng-navy);
}

.paj-eng-swatch__tick {
	font-size: 15px;
	font-weight: 700;
	line-height: 1;
	color: var(--pf-tick);
	opacity: 0;
	transition: opacity 150ms;
}

/* Always in the DOM, opacity-toggled, so the design's conditional render costs
   zero JS, and a checkmark (not a border alone) always conveys selection. */
.paj-eng-swatch[aria-checked='true'] .paj-eng-swatch__tick {
	opacity: 1;
}

/* =====================================================================
 * Reveals
 * ================================================================== */

/* Default state shows everything. The collapsing class is what hides the
   overflow, so no-JS renders the complete set rather than a truncated one. */
.paj-eng-fonts.is-collapsed [data-extra='1'],
.paj-eng-colours.is-collapsed [data-extra='1'] {
	display: none;
}

.paj-eng .paj-eng-reveal {
	align-self: flex-start;
	background: none;
	border: none;
	padding: 8px 0;

	/* 4px + the 8px top padding = 12px of visual air under the chips. Was
	   -4px, which left ~4px: on the live render the link sat pressed against
	   the chip row (design review, 2026-08-10). */
	margin-top: 4px;
	color: var(--paj-eng-link);
	font-size: 14px;
	font-weight: 500;
	text-decoration: underline;
	cursor: pointer;
}

.paj-eng .paj-eng-reveal:hover {
	text-decoration: none;
}

.paj-eng .paj-eng-reveal-dot {
	width: 36px;
	height: 36px;
	border-radius: 50% !important;
	background: #fff;
	border: 1px dashed var(--paj-eng-disabled);
	color: var(--paj-eng-link);
	font-size: 18px;
	line-height: 1;
	cursor: pointer;
	display: flex;
	align-items: center;
	justify-content: center;
	flex: none;
	transition: background-color 200ms, border-color 200ms;
}

.paj-eng .paj-eng-reveal-dot:hover {
	background: var(--paj-eng-surface);
	border-color: var(--paj-eng-muted);
}

.paj-eng-capsnote {
	font-size: 13px;
	color: var(--paj-eng-muted);
	margin: -4px 0 0 !important;
}

/* =====================================================================
 * Summary
 * ================================================================== */

.paj-eng-summary {
	display: flex;
	align-items: center;
	gap: 10px;
	font-size: 14px;
	padding: 10px 12px;
	background: var(--paj-eng-surface);
	border-radius: var(--paj-eng-radius-sm);
	margin-top: 12px;
}

.paj-eng-summary__chip {
	width: 52px;
	height: 34px;
	background: var(--paj-eng-plate);
	border-radius: 5px;
	display: flex;
	align-items: center;
	justify-content: center;
	flex: none;
	overflow: hidden;
}

.paj-eng-summary__text-inner {
	font-family: var(--paj-eng-fam);
	font-weight: var(--paj-eng-weight);
	font-size: var(--paj-eng-summary-size);
	color: var(--paj-eng-ink);
	text-transform: var(--paj-eng-caps, none);
	/* No text-shadow — D22, see .paj-eng-card__engrave. */
	white-space: nowrap;
	line-height: 1;
}

.paj-eng-summary__label {
	flex: 1 1 auto;
	min-width: 0;
	color: var(--paj-eng-navy);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.paj-eng .paj-eng-link {
	background: none;
	border: 0;
	padding: 0;
	color: var(--paj-eng-link);
	font-size: 14px;
	font-weight: 500;
	text-decoration: underline;
	cursor: pointer;
	flex: none;
}

/* =====================================================================
 * Price
 * ================================================================== */

.paj-eng-price {
	margin-top: 12px;
}

/* Hardened tier + !important on the type: same page, same hostile cascade as
   the card sub — this block also wraps wc_price() output (0.8.1). */
.paj-eng .paj-eng-price__breakdown {
	font-size: 13px !important;
	color: var(--paj-eng-muted);
	margin: 0 0 2px !important;
}

.paj-eng .paj-eng-price__total {
	font-size: 15px !important;
	margin: 0 !important;
}

.paj-eng .paj-eng-price__label {
	font-weight: 500 !important;
	margin-right: 6px;
}

.paj-eng .paj-eng-price__amount {
	font-weight: 700 !important;
	font-size: 20px !important;
}

/* Germanized's suffix travels with the composed total, because PAngV requires the VAT
   statement wherever a price is advertised. */
.paj-eng .paj-eng-price__suffix {
	font-size: 12px !important;
	color: var(--paj-eng-muted);
	margin-left: 6px;
}

.paj-eng .paj-eng-price__suffix small {
	font-size: inherit !important;
}

/* Inherit-pinning for the wc_price() markup inside this block: the block's
   own spans state the design; the Woo spans must not restate it. `inherit`
   rather than a weight — the muted 13px breakdown line wraps wc_price() too
   and must not go bold. */
.paj-eng .paj-eng-price .woocommerce-Price-amount,
.paj-eng .paj-eng-price .woocommerce-Price-amount bdi,
.paj-eng .paj-eng-price .woocommerce-Price-currencySymbol {
	font-family: inherit !important;
	font-size: inherit !important;
	font-weight: inherit !important;
	color: inherit !important;
}

/* =====================================================================
 * Spelling confirmation
 * ================================================================== */

.paj-eng-spell {
	display: flex;
	gap: 10px;
	align-items: flex-start;
	font-size: 13px;
	color: var(--paj-eng-muted);
	cursor: pointer;
	margin-top: 12px;
	padding: 8px;
	border-radius: var(--paj-eng-radius-sm);
	transition: background-color 300ms;
	outline: none !important;
}

.paj-eng-spell.is-flash {
	background: #fde7df;
}

.paj-eng-spell__box {
	width: 18px;
	height: 18px;
	margin: 1px 0 0;
	accent-color: var(--paj-eng-orange);
	flex: none;
}

.paj-eng-spell strong {
	color: var(--paj-eng-navy);
}

/* The reference-board link, inline inside the confirmation sentence. The
   scoped reset has no `.paj-eng a` tier, so an anchor in here inherits from
   whatever the theme says about links; --paj-eng-link is the same token the
   "Alle Schriften" reveal uses, so the page's two links match. !important on
   colour and decoration to clear the (0,2,1) floor — the convention in this
   file, not debt. */
.paj-eng .paj-eng-spell__ref {
	color: var(--paj-eng-link) !important;
	text-decoration: underline !important;
	text-underline-offset: 2px;
}

.paj-eng .paj-eng-spell__ref:hover {
	text-decoration: none !important;
}

/* =====================================================================
 * The colour/font reference popup (D25)
 *
 * Opened from .paj-eng-spell__ref above. A native <dialog> driven by
 * showModal(), which is not a style choice: .paj-eng sets
 * container-type: inline-size (see "Root layout"), so it is the containing
 * block for fixed-position descendants and a position:fixed overlay of ours
 * would be clipped by OUR OWN stylesheet before Flatsome, Elementor or
 * CheckoutWC got a vote. The top layer is measured against the viewport and
 * containment does not reach it.
 *
 * ⚠️ Custom properties still resolve in here. showModal() changes where the
 * element PAINTS, not where it sits in the tree, so the dialog remains a DOM
 * descendant of .paj-eng and inherits --paj-eng-* normally. ::backdrop is the
 * one exception and is handled at its own rule.
 *
 * ⚠️ @media, never @container. The container is .paj-eng — the configurator
 * column — so a container query would size a full-viewport modal by the width
 * of the column behind it.
 * ================================================================== */

/*
 * 🔴 display BELONGS ON [open] ONLY.
 *
 * The UA closes a dialog with `dialog:not([open]) { display: none }` at (0,1,1).
 * A plain `.paj-eng .paj-eng-refmodal { display: flex }` is (0,2,0) and beats it,
 * which would leave the CLOSED dialog rendered inline and permanently — the whole
 * chart printed above the CTA on the best-converting page in the shop. Exactly the
 * trap the `.paj-eng [hidden]` blanket rule documents further up, so the fix is
 * borrowed from it: the belt below states the closed case explicitly rather than
 * trusting the UA sheet to outrank us.
 *
 * :not([open]) ADDS an attribute selector, it does not lower specificity — this is
 * not the :where()/:is() hazard.
 */
.paj-eng .paj-eng-refmodal:not([open]) {
	display: none !important;
}

/* The dialog itself is the full-viewport hit area and is deliberately invisible:
   every painted surface lives on __panel. That is what lets engraving.js treat
   "event.target IS the dialog" as a backdrop click without hit-testing geometry.
   All four UA box declarations (border, padding, background, max-*) are reset. */
.paj-eng .paj-eng-refmodal {
	position: fixed;
	inset: 0;
	width: 100%;
	height: 100%;
	max-width: none;
	max-height: none;
	margin: 0;
	padding: 16px;
	border: 0;
	background: transparent;
	color: var(--paj-eng-navy);
	overflow: hidden;
}

.paj-eng .paj-eng-refmodal[open] {
	display: flex;
	align-items: center;
	justify-content: center;
}

/* ::backdrop gets a LITERAL colour, never var(). Whether it inherits custom
   properties from its originating element is a recent and unevenly implemented
   spec change; where it does not, a var() reference resolves to nothing and the
   backdrop renders transparent, leaving the sheet floating over a live page. */
.paj-eng .paj-eng-refmodal::backdrop {
	background: rgba(26, 40, 57, 0.72);
}

.paj-eng .paj-eng-refmodal__panel {
	display: flex;
	flex-direction: column;
	width: min(92vw, 960px);
	max-height: 92vh;
	/* Two-declaration fallback, the idiom used for the narrow-viewport block: a
	   browser that does not know dvh keeps the vh line above. Worth having because
	   mobile Safari's toolbars are exactly what vh gets wrong. */
	max-height: 92dvh;
	background: #fff;
	border: 1px solid var(--paj-eng-border);
	border-radius: var(--paj-eng-radius);
	overflow: hidden;
}

.paj-eng .paj-eng-refmodal__head {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	flex: none;
	padding: 14px 16px;
	background: var(--paj-eng-surface);
	border-bottom: 1px solid var(--paj-eng-border);
}

/*
 * ⚠️ The header is EXACTLY full at 375px, verified rather than assumed: the title
 * measures 295px, the gap is 12 and the button 36, which is 343 — the content box to
 * the pixel. It does not overflow, because a flex item's automatic minimum size comes
 * from its MIN-CONTENT width (the longest word, ~100px here) and not its one-line
 * width, so longer copy wraps to a second line and the button holds its place. An
 * explicit min-width:0 was tried and measured as a no-op for that reason, then
 * removed rather than left as decoration.
 *
 * What would break it is a title with one very long unbreakable token, which raises
 * min-content above the space available and pushes the button out through the panel's
 * overflow:hidden. That matters more than it sounds: below 600px the panel is
 * full-bleed, so there is no backdrop left to tap and Escape needs a keyboard — the
 * button is the ONLY way a phone user closes this. The UI suite asserts the button's
 * box stays inside the panel's.
 */
.paj-eng .paj-eng-refmodal__title {
	margin: 0 !important;
	font-size: 16px;
	font-weight: 600;
	line-height: 1.3;
	color: var(--paj-eng-navy);
}

/* (0,2,0) beats the (0,1,1) theme reset, so no !important is needed for the box
   properties here — see the reset's own note. */
.paj-eng .paj-eng-refmodal__close {
	flex: none;
	width: 36px;
	height: 36px;
	display: flex;
	align-items: center;
	justify-content: center;
	font-size: 26px;
	line-height: 1;
	color: var(--paj-eng-muted);
	background: none;
	border: 0;
	border-radius: var(--paj-eng-radius-sm);
	cursor: pointer;
}

.paj-eng .paj-eng-refmodal__close:hover {
	color: var(--paj-eng-navy);
	background: #e2e8f0;
}

/*
 * 🔴 A DELIBERATE EXCEPTION to "no focus outline on any button in the
 * configurator" (PAJ, 2026-08-06, see the reset above). That request is sound for
 * the chip and swatch rows: the roving tabindex keeps each row one tab stop and
 * arrow keys move a selection the customer can already see. NEITHER is true here.
 * showModal() TRAPS focus, so a keyboard user with no visible ring cannot find the
 * way out of a modal at all — a different failure from "which chip am I on", and
 * BFSG has applied to this shop since June 2025.
 *
 * Scoped to this one button and to :focus-visible only, so a mouse click still
 * shows no ring and the case that prompted PAJ's request does not come back.
 * (0,3,0) outranks the reset's (0,2,1) on the class column, so this needs no
 * !important. Recorded in D25 rather than applied quietly.
 */
.paj-eng .paj-eng-refmodal__close:focus-visible {
	outline: 2px solid var(--paj-eng-link);
	outline-offset: 2px;
}

/* The scroll container. overscroll-behavior: contain instead of locking the body:
   a scroll lock has to be added and removed by JS, and the failure mode when the
   remove is missed is a shop page that cannot be scrolled at all. Containing the
   chain costs nothing and cannot strand anyone. */
.paj-eng .paj-eng-refmodal__body {
	flex: 1 1 auto;
	overflow: auto;
	overscroll-behavior: contain;
	-webkit-overflow-scrolling: touch;
	padding: 16px;
	background: #fff;
}

/* Fit to width, at EVERY width — the narrow block below deliberately adds no width
   of its own. The pages are square since the -c artwork (2200x2200; -a and -b were
   2200x2324), so at >=600px the panel is wide enough for a twelve-face or sixteen-ink
   grid to stay readable; below it, see the trade recorded at that breakpoint. The rule
   itself is width:100% / height:auto and assumes NO aspect, so a future letter of a
   different shape needs no CSS change — only the w/h in products.json. */
.paj-eng .paj-eng-refmodal__page {
	display: block;
	width: 100%;
	max-width: 100%;
	height: auto;
	border-radius: var(--paj-eng-radius-sm);
}

.paj-eng .paj-eng-refmodal__page + .paj-eng-refmodal__page {
	margin-top: 16px;
}

.paj-eng .paj-eng-refmodal__foot {
	flex: none;
	padding: 12px 16px;
	background: var(--paj-eng-surface);
	border-top: 1px solid var(--paj-eng-border);
}

/* No .paj-eng a tier exists in the theme reset, so this matches .paj-eng-spell__ref
   and uses !important for the same reason that rule does. */
.paj-eng .paj-eng-refmodal__pdf {
	font-size: 13px;
	font-weight: 500;
	color: var(--paj-eng-link) !important;
	text-decoration: underline !important;
	text-underline-offset: 2px;
}

.paj-eng .paj-eng-refmodal__pdf:hover {
	text-decoration: none !important;
}

/*
 * Narrow viewports. A NEW TOP-LEVEL @media, deliberately not folded into the
 * (max-width: 600px) block further down — that one is nested inside
 * @supports not (container-type: inline-size) and therefore never matches on any
 * browser that supports container queries, which is all of them. Putting these
 * rules there would ship the desktop panel geometry to exactly the devices this
 * breakpoint exists for: an inset, boxed panel instead of a full-bleed one, costing
 * the chart the ~32px it needs most.
 *
 * 🔴 THE PAGES FIT THE PANEL HERE. They are deliberately NOT widened past it, and
 * this block declares no width on __page at all — the base rule's 100% stands.
 * Until 0.9.20 it forced 860px so the customer panned sideways; that was reported as
 * a defect on the first phone it was seen on (Johnny, 2026-08-12), because a sheet
 * running off the right edge reads as a broken page rather than as an invitation to
 * pan, and no amount of hint copy fixes that first impression.
 *
 * The cost is real and is stated rather than hidden: a near-square 12-face chart at
 * ~360px puts the row labels near 5px. So at this width the sheet shows WHICH faces
 * and inks exist and roughly how each sits on a black housing, and the two paths to
 * the names are browser pinch-zoom — the rasters are 2200px wide, six times the
 * displayed size, so it stays sharp — and "Als PDF oeffnen" in the footer. Both are
 * now UNSIGNPOSTED: __hint said so in words until 0.9.25 and was removed as clutter
 * above the sheet (Johnny, 2026-08-13). The durable fix is a one-column portrait
 * re-export shipped as -b, not a CSS rule; D25 already expects that re-export for
 * the English ink names.
 *
 * ⚠️ Do not restore a fixed px width on __page here. The UI suite asserts this block
 * declares none, precisely because the 860px it replaced sat behind a breakpoint the
 * harness does not run at and was never questioned by a check.
 */
@media (max-width: 600px) {
	.paj-eng .paj-eng-refmodal {
		padding: 0;
	}

	.paj-eng .paj-eng-refmodal__panel {
		width: 100%;
		max-width: none;
		max-height: 100vh;
		max-height: 100dvh;
		border: 0;
		border-radius: 0;
	}

	/* 8px rather than the base 16px: with the pages fitted, side padding comes
	   straight off the chart, and at 375px this is about 5% more drawing. */
	.paj-eng .paj-eng-refmodal__body {
		padding: 8px;
	}
}

/* =====================================================================
 * Hint and notes
 * ================================================================== */

/* aria-hidden in the markup: the same message is announced through
   #paj-eng-live on a guarded click, and a screen reader must not hear it twice. */
.paj-eng-hint {
	margin: 8px 0 0 !important;
	font-size: 13px;
	color: var(--paj-eng-orange);
	font-weight: 500;
}

/* .paj-eng-notes removed 2026-08-06 with the two "Gravur:" lines it styled. See
   the note in class-shortcode.php where the markup was. */

/* Phase-1 style warning banners, kept for the flag-on-locally state. */
.paj-eng-warn {
	background: #fcf9e8;
	border-left: 4px solid #dba617;
	padding: 8px 12px;
	font-size: 13px;
	margin: 0 0 12px !important;
}

.paj-eng-warn--block {
	background: #fcf0f1;
	border-left-color: #b32d2e;
}

/* =====================================================================
 * Animations
 * ================================================================== */

/* Prefixed pajEng*, not the design's pf*, because keyframe names are global and
   two-letter prefixes on a page running Elementor + Flatsome are collision bait. */
/* pajEngShimmer removed 2026-08-11 (0.9.4) with the glare and the photo sweep it
   drove — see the notes at both rule sites. */

@keyframes pajEngFadeUp {
	from {
		opacity: 0;
		transform: translateY(10px);
	}
	to {
		opacity: 1;
		transform: translateY(0);
	}
}

.paj-eng-fadeup {
	animation: pajEngFadeUp 300ms ease-out;
}

/* .paj-eng-card__glare / .paj-eng-chip__glare removed 2026-08-11 (0.9.4) with the
   spans they painted (class-shortcode.php).

   🔴 Do not put a 105deg white band back on the plates, and understand why before
   arguing for it. It never behaved as a sweep. shimmer() armed it by adding
   .is-shimmer to the root and NOTHING ever removed the class, while
   animation-fill-mode:both held the last keyframe — so after the first font,
   colour or card click a 45%-wide white gradient parked at translateX(140%) and
   stayed at opacity 1 until the next page load. On the chips (80px, overflow:
   hidden, #17181c) that is a permanent haze over the right third of all twelve; on
   the named card's plate, which has NO overflow:hidden, it lay across the right of
   the device render and spilled onto the card. Under prefers-reduced-motion the
   animation was cancelled but the opacity was not, so it simply parked over the
   LEFT 45% instead. This is what PAJ reported on 2026-08-11 as light and shadow on
   the background, in both places, and it is the same synthetic-surface family the
   plate fill 0.9.1 removed (D14). If a plate effect is ever genuinely wanted,
   clear the class on animationend — never ship one that parks. */

@media (prefers-reduced-motion: reduce) {
	.paj-eng *,
	.paj-eng *::before,
	.paj-eng *::after {
		animation: none !important;
		transition: none !important;
	}
	.paj-eng-panel {
		transition: none !important;
	}
}

/* =====================================================================
 * Narrow viewports
 * ================================================================== */

@container (max-width: 420px) {
	/* The base .paj-eng .paj-eng-input declarations are !important (Elementor
	   kit, see the name-field block), and an important declaration outranks a
	   plain one regardless of the media/container context — so these narrow
	   overrides must be !important too, or the 64px base silently wins. */
	.paj-eng .paj-eng-input {
		height: 56px !important;
		font-size: 20px !important;
		padding-right: 56px !important;
	}
	.paj-eng .paj-eng-card {
		padding: 13px 9px 11px;
	}
	.paj-eng .paj-eng-card[aria-checked='true'] {
		padding: 12px 8px 10px;
	}
	/* 0.8.1: the engrave text is cqw-sized against the plate, so the narrow width
	   scales it automatically — no font override needed on THIS path. 89, not 76,
	   since 0.9.7: 76 × 112/96 holds the same ratio to the wide plate. The
	   @supports fallback below is the path that does need a font override, because
	   cqw never resolves there. */
	.paj-eng-card__plate {
		width: calc(var(--paj-geo-plate-w-narrow) * 1px);
	}
}

/* Fallback for browsers without container queries. The gallery snippet's own
   breakpoint is 600px, so matching it keeps the two in step. */
@supports not (container-type: inline-size) {
	/* The card engrave sizes itself in cqw, which never resolves here. The
	   two-declaration fallback idiom does NOT work for it: cqw sits inside
	   calc() with var(), which parses fine and only fails at computed-value
	   time, collapsing font-size to inherit instead of falling back. So the
	   JS-computed pixel size stays the fallback path — at every width, hence
	   outside the @media. !important because the base declaration is.

	   This is the ONLY consumer of --paj-eng-plate-size, and engraving.js derives
	   its boxW from --paj-geo-plate-w x --paj-geo-window-w rather than carrying a
	   copy of either (plateBoxW). That matters precisely because nothing on a
	   modern browser reaches this rule, so a hardcoded mirror here could rot for
	   years and only ever be seen by an old-Safari customer. */
	.paj-eng .paj-eng-card__engrave {
		font-size: var(--paj-eng-plate-size) !important;
	}

	@media (max-width: 600px) {
		/* !important for the same reason as the container-query block above. */
		.paj-eng .paj-eng-input {
			height: 56px !important;
			font-size: 20px !important;
			padding-right: 56px !important;
		}
		.paj-eng-card__plate {
			width: calc(var(--paj-geo-plate-w-narrow) * 1px);
		}

		/* The plate above is narrower, so the name must be too. Without this the
		   rule outside the @media keeps the 112px-window size and the name
		   overflows the 89px window by 26% into overflow:hidden — the ascender
		   flourish and descender sliced flat, on phones, on this path only. That
		   is a real report (PAJ, 2026-08-12, "gutted on some phones"), and the
		   comment this replaces argued the opposite: that the cqw path scaling
		   automatically meant no font override was needed. True there, false
		   here, and here is the only place the literal ever applied. */
		.paj-eng .paj-eng-card__engrave {
			font-size: var(--paj-eng-plate-size-narrow) !important;
		}
	}
}

/* =====================================================================
 * Nodes OUTSIDE our root
 *
 * These classes are ours; the elements are Elementor's, Flatsome's or
 * WooCommerce's. Kept unscoped by necessity and deliberately minimal: every
 * rule here is one we could be asked to justify.
 * ================================================================== */

/* The guarded add-to-cart button. aria-disabled, never the disabled attribute:
   a disabled button is skipped by screen readers and offers no reason.
   !important on every declaration: this class sits on a THEME button, where
   Flatsome/Elementor kit rules outrank a bare (0,1,0) selector (0.8.1's
   hostile-CSS lesson; the .paj-eng ancestor tier is unavailable out here).
   Deliberately NO pointer-events:none — the click must reach the guard so it
   can route the customer to the missing field. And no [aria-disabled]
   selector: Flatsome deletes that attribute at will (D13); the class is the
   one bit gate and visuals share. */
.paj-eng-guarded {
	opacity: 0.55 !important;
	cursor: not-allowed !important;
	filter: grayscale(0.35) !important;
}

/* Used to hide a foreign price node while our composed total sits beside it —
   in the sticky bars and, since 0.7.0, in the main Elementor price widget. We
   never rewrite their text, so restoring is removing one class. */
.paj-eng-hidden {
	display: none !important;
}

.paj-eng-sticky-total {
	font-weight: 700;
}

/* ---------------------------------------------------------------------
 * The live engraving on the gallery plate slide
 *
 * Built by the plateSlide integration in engraving.js, which wraps the last
 * slide's <img> rather than positioning against .paj-main-slide. Read that
 * function's docblock before changing anything here: several of these
 * declarations are load-bearing against the gallery's own CSS and against its
 * drag and lightbox handlers.
 * ------------------------------------------------------------------ */

.paj-eng-plate {
	position: relative;
	display: block;

	/*
	 * The wrapper's box must EQUAL the image's box, or the percentage window
	 * geometry below lands in the wrong place. aspect-ratio matches the blank
	 * render's intrinsic 290x196 and the img rule underneath fills it exactly, so
	 * there is no letterboxing to compensate for.
	 */
	aspect-ratio: 290 / 196;

	/*
	 * 78%, not 100%. The slide is a 1:1 box up to ~600px wide while the render is
	 * only 290px, so filling the slide upscales it past 2x and the housing goes
	 * soft. Capping it reads as a deliberately centred product render instead.
	 *
	 * Left at 78% through the 0.9.7 render swap deliberately. The new render
	 * frames the device more loosely, so the DEVICE draws smaller here than it
	 * did — but at the same ~1.6x upscale of its own source pixels, which is
	 * exactly what this cap is protecting. Raising it to buy back apparent size
	 * spends resolution instead. Low stakes either way: this is the FALLBACK
	 * surface and plateFull is '' whenever the hero photo exists, which it does.
	 */
	width: min(100%, 78%);

	/* cqw units in .paj-eng-plate__mark resolve against this. */
	container-type: inline-size;

	/*
	 * 🔴 Both required. .paj-main-slider carries mousedown/touchstart drag
	 * handlers and every .paj-main-slide has a click listener calling
	 * openLightbox(i). Without these a mouse drag selects the engraved text
	 * instead of swiping the gallery, and the mobile lightbox tap mis-fires.
	 * .paj-main-slide img already sets pointer-events:none for the same reason.
	 */
	pointer-events: none;
	user-select: none;
	-webkit-user-select: none;
}

/*
 * !important is unavoidable here, not laziness. The gallery snippet styles its
 * images as `#pajGallery_<id> .paj-main-slide img`, which carries an ID and
 * therefore outranks any class selector we can write, and it sets
 * height:100% + object-fit:contain. Those two are exactly what would letterbox
 * the render inside its element and invalidate the window percentages.
 */
.paj-eng-plate img {
	width: 100% !important;
	height: 100% !important;
	object-fit: fill !important;
	display: block;
}

/*
 * The engraving window, over the same image as .paj-eng-card__engrave and
 * therefore the SAME calibration — literally, not by transcription: both rules
 * read --paj-geo-window-*, so the card and the product image cannot disagree
 * about where the mark sits, including the clip-box height. Until 0.9.12 this
 * was a hand-kept copy with a comment asking the next reader to change it in
 * both places. Read the 🔴 note over .paj-eng-card__engrave; recalibrate in the
 * token block, never here.
 */
.paj-eng-plate__mark {
	position: absolute;
	left: calc(var(--paj-geo-window-l) * 1%);
	top: calc(var(--paj-geo-window-t) * 1%);
	width: calc(var(--paj-geo-window-w) * 1%);
	height: calc(var(--paj-geo-window-h) * 1%);
	display: flex;
	align-items: center;
	justify-content: center;
	white-space: nowrap;
	overflow: hidden;
	line-height: 1;

	font-family: var(--paj-eng-fam);
	font-weight: var(--paj-eng-weight);
	color: var(--paj-eng-ink);
	text-transform: var(--paj-eng-caps, none);
	/* No text-shadow — D22, see .paj-eng-card__engrave. This mark and the card's
	   are one calibration and must stay identical; that includes carrying no
	   relief, not just sharing the same window numbers. */

	/*
	 * fitPlate() from engraving.js, transcribed into CSS:
	 *   min( boxW * margin / (len * adv), boxH * margin / box ) * scale
	 * with boxW expressed in cqw, i.e. this box's own width — the same token the
	 * width above uses, so the type can never be fitted to a window of a
	 * different size than the one it sits in. Doing it this way rather than
	 * publishing a fourth px variable is what removes the need to measure the
	 * slide and to re-measure it on every resize, orientation change and
	 * thumbnail-rail reflow. boxH comes from the same window token times
	 * --paj-geo-plate-aspect, because cqw is a share of WIDTH and
	 * --paj-geo-window-h is a percentage of HEIGHT.
	 *
	 * 🔴 Must stay character-for-character the same shape as
	 * .paj-eng-card__engrave — read the note there for what the two arms are and
	 * why the font-blind pair they replaced in 0.9.22 could only ever draw a face
	 * smaller than its window (D27). This surface showed the identical defect for
	 * the identical reason, as it did for the horizontal cut in 0.9.21 (D26); the
	 * two are one calibration, and fixing one and not the other is how they drift.
	 */
	font-size: calc(min(
		(var(--paj-geo-window-w) * 1cqw) * var(--paj-eng-adv-margin, 0.94) / (var(--paj-eng-len, 5) * var(--paj-eng-adv, 0.62)),
		(var(--paj-geo-window-h) * var(--paj-geo-plate-aspect) / 100 * 1cqw) * var(--paj-eng-adv-margin, 0.94) / var(--paj-eng-box, 1.35)
	) * var(--paj-eng-scale, 1));
}

/*
 * content, not textContent. paintVars already publishes --paj-eng-name as a
 * JSON-quoted string, which is precisely the form CSS content requires, so the
 * name reaches a surface outside our root with no JS text handling at all.
 */
.paj-eng-plate[data-paj-eng-plate-state='named'] .paj-eng-plate__mark::before {
	content: var(--paj-eng-name, '');
}

/* Standard variant: the wordmark, matching the one printed on the standard
   card's render (the CSS overlay it once mirrored was retired in 0.8.1).
   Hardcoded rather than read from strings-de.json because CSS cannot read
   config, and because it is a wordmark rather than a translatable phrase.

   5cqw stands through the 0.9.7 window change: "PAJ GPS" at this size is about
   24cqw wide and the window is --paj-geo-window-w cqw, so it still fits. Not a
   var() itself: this is a fixed wordmark size, not a fitted one. It is deliberately not
   re-fitted to the printed wordmark's 33.4cqw — that bbox covers the logo glyph
   AND the text, and this rule draws only the text. */
.paj-eng-plate[data-paj-eng-plate-state='standard'] .paj-eng-plate__mark {
	color: #c9ccd2;
	font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
	font-weight: 700;
	font-size: 5cqw;
	letter-spacing: 0.1em;
	text-transform: none;
}

.paj-eng-plate[data-paj-eng-plate-state='standard'] .paj-eng-plate__mark::before {
	content: 'PAJ GPS';
}

/* No container-query support: fall back to a fraction of the viewport, which is
   wrong in detail but keeps a readable mark rather than an unstyled one. Scaled
   by 36.6/52 in 0.9.7 with the window it has to fit inside; overflow:hidden
   would otherwise clip a name this path used to show. Deliberately NOT derived
   from --paj-geo-window-w: vw has no relation to the plate's own width, so this
   is an eyeballed viewport ratio rather than a transcription of the window.

   The width correction below is a RATIO, not a fit, and that is the honest shape
   for this path: there is no window in scope to fit against, so it asks only
   "how much wider than the face this was eyeballed for is the face now?" and
   gives back exactly that. Capped at 1 so this can never enlarge a mark on a
   path nobody can test (D26).

   🔴 THE NUMERATOR MUST EQUAL THE FALLBACK, and both moved 0.52 -> 0.62 in
   0.9.22 with --paj-eng-adv itself. The pair is what makes an unmeasured face
   come out at exactly 1, i.e. unchanged — that is the whole intent, and leaving
   the numerator at 0.52 while the fallback became 0.62 would have quietly shrunk
   the four OS faces by 16% on the one path nobody can test. Measured faces are
   unaffected by the pairing: opt-01's 0.758 gives 0.818 where it gave
   0.686, which is the same statement about the same two faces read against the
   new baseline. */
@supports not (container-type: inline-size) {
	.paj-eng-plate {
		container-type: normal;
	}
	.paj-eng-plate__mark {
		font-size: calc(min(2.8vw, 20px) * var(--paj-eng-scale, 1) * min(1, 0.62 / var(--paj-eng-adv, 0.62)));
	}
	.paj-eng-plate[data-paj-eng-plate-state='standard'] .paj-eng-plate__mark {
		font-size: min(1.4vw, 11px);
	}
}

/* ---------------------------------------------------------------------
 * The live engraving on the gallery PHOTO slide (the hero dog photo)
 *
 * Built by the photoSlide integration in engraving.js — the primary preview
 * surface; the plate block above is its fallback. Same load-bearing rules as
 * .paj-eng-plate: read the plateSlide docblock before changing anything.
 *
 * PAINT ONLY. The mark's position, size and rotation arrive as inline styles
 * from products.json gallery_photo.engrave, so recalibrating against a new
 * photo is a JSON edit, never an edit here.
 *
 * One fact the plate comments predate: the snippet's own late CSS block (the
 * [data-peek] rules, outside its media query) gives EVERY .paj-main-slide
 * position:relative and its img position:absolute + object-fit:contain at
 * every viewport. Wrapping the img re-anchors that absolute positioning to
 * this wrapper — exactly what we want — but it is also why the wrapper must
 * fill the slide's square box itself rather than trusting the img to size it.
 * ------------------------------------------------------------------ */

.paj-eng-photo {
	position: relative;
	display: block;

	/*
	 * The photo is 1:1 and the slide box is 1:1, so unlike the plate's 78% cap
	 * the wrapper fills the slide edge to edge: the render is 1024px, never
	 * upscaled past ~2x. aspect-ratio (not height:100%) keeps the wrapper's box
	 * EQUAL to the image's box even if the slide box ever stops being square —
	 * the percentage window survives; the slide's overflow:hidden does the rest.
	 */
	width: 100%;
	aspect-ratio: 1 / 1;

	/* cqw units below resolve against this. */
	container-type: inline-size;

	/* Clip the mark and pill exactly like the snippet clips its img. */
	border-radius: 16px;
	overflow: hidden;

	/* 🔴 Both required — same drag/lightbox hazards as .paj-eng-plate. */
	pointer-events: none;
	user-select: none;
	-webkit-user-select: none;
}

/*
 * Same specificity fight as the plate: the snippet styles its images as
 * `#pajGallery_<id> … img`, which outranks any class selector we can write.
 * cover, not fill: 1:1 into 1:1 they are identical, but if the wrapper is ever
 * not square, cover keeps the photo undistorted while the window percentages
 * stay anchored to the wrapper.
 */
.paj-eng-photo img {
	width: 100% !important;
	height: 100% !important;
	object-fit: cover !important;
	display: block;
}

/*
 * The engraving window. NO BACKGROUND, and that is the point of it: the mark
 * is drawn straight onto the tracker in the photograph.
 *
 * 🔴 Do not give this a fill again. Until 0.9.1 it painted an opaque radial
 * gradient with a border-radius and inset shadows — the design's "synthetic
 * plaque" from PET Finder Hero.dc.html — because the photo of the day
 * (dog-gold.jpg) had the name 'Bailey' baked into the tracker pixels and
 * something had to cover it. On a black device that plate read as a black
 * sticker stuck to a black surface, which is what PAJ reported: "too fake,
 * like a double background". The photo was replaced with an unengraved render
 * (products.json gallery_photo._clean_render_note), so there is nothing left
 * to hide, and a fill now buys nothing and costs the illusion.
 *
 * overflow:hidden stays: it keeps a long name inside the housing rather than
 * letting it run across the dog. Geometry (left/top/width/height/transform)
 * is inline, from products.json.
 */
.paj-eng-photo__mark {
	position: absolute;
	display: flex;
	align-items: center;
	justify-content: center;
	white-space: nowrap;
	overflow: hidden;
	line-height: 1;
}

/* Named variant: the customer's name, cut into the housing. */
.paj-eng-photo[data-paj-eng-photo-state='named'] .paj-eng-photo__mark {
	font-family: var(--paj-eng-fam);
	font-weight: var(--paj-eng-weight);
	color: var(--paj-eng-ink);
	text-transform: var(--paj-eng-caps, none);
	letter-spacing: 0.02em;

	/*
	 * No text-shadow — D22.
	 *
	 * This surface carried the most developed version of the relief: shadow
	 * ABOVE and an ink highlight BELOW, calibrated to the photograph's
	 * upper-left key light, in cqw so it tracked the photo instead of staying
	 * 1px. It is removed with the other five anyway. PAJ's instruction was about
	 * the letters themselves — "any shadow or any outline or any border … inside
	 * the font" — and a name that is relieved here but flat on the card and the
	 * chips is the inconsistency that reads as a bug.
	 *
	 * This was the ONLY CSS consumer of --paj-eng-ink-hi. That variable is
	 * deliberately kept: engraving.js writes it, ui-test-suite.js asserts it, and
	 * it is registry-backed (hex_hi, editable in wp-admin). Retiring it is a
	 * registry decision, not a styling one. See the note at its declaration.
	 */

	/*
	 * 🔴 THE SECOND ARM WAS THE DESIGN'S OWN FORMULA UNTIL 0.9.22, and it is the
	 * reason PAJ reported this surface as "not clear on the dog device" (D27).
	 *
	 * It was `2.6cqw * scale * min(1, 6.5/len)`: a fixed size and a character
	 * count, both font-blind, both eyeballed against a normal-width face. On
	 * opt-09 at the 8-character cap it resolved to 2.32cqw while the window had
	 * room for 4.22 — the name was drawn at 55% of the size the tracker in the
	 * photograph could hold, on the largest and most-looked-at preview in the
	 * product. 0.9.21 had already put a real window constraint in FRONT of it
	 * (D26), which caught the overflow on opt-01 but could not do
	 * anything about the reverse, because a min() cannot grow a face.
	 *
	 * Both arms are measurements now, matching .paj-eng-card__engrave. Read the
	 * note there; the only differences are local and both are geometric:
	 *
	 *   - the window is --paj-eng-photo-w/-h, published inline by photoSlide()
	 *     from the same products.json gallery_photo.engrave numbers that set
	 *     `width`/`height` above, so the fit and the box cannot disagree. This
	 *     window is calibrated against a PHOTOGRAPH of a real tracker and is NOT
	 *     the card's window — do not widen it to match; there is no plate render
	 *     underneath, only pixels of a device at a particular angle.
	 *   - no --paj-geo-plate-aspect: the photo wrapper is square (1:1 into 1:1,
	 *     see the img rule), so a percentage of its height and a percentage of
	 *     its width are the same cqw. If that wrapper ever stops being square
	 *     this arm needs the same aspect conversion the plate uses.
	 *   - the + 0.02 is letter-spacing: it adds 0.02em per character, including a
	 *     trailing one, so it is exactly an addition to the per-character advance
	 *     and belongs inside the divisor. It has no vertical equivalent.
	 */
	/*
	 * 🔴 THE THIRD ARM IS A CEILING, NOT A FIT (0.9.30, D34), AND IT SITS OUTSIDE
	 * THE SCALE ON PURPOSE.
	 *
	 * The two arms above answer "how large may this name be before it stops
	 * fitting its window". They cannot answer "how large does the factory actually
	 * print", and without that a 3-character name was drawn at 4.1-5.4% of the
	 * photo against the real wordmark's 3.33% — filling its window perfectly and
	 * still looking nothing like a print. --paj-eng-photo-max is that missing
	 * number, measured off the wordmark itself; products.json _max_em_comment
	 * carries the derivation and the reason it is invisible at the 8-character cap.
	 *
	 * OUTSIDE min() AND OUTSIDE THE SCALE, which is the whole distinction: a fit
	 * arm is a statement about the window and must be scaled with the face, a
	 * ceiling is a statement about the printed result and must not — a face
	 * carrying preview_scale 1.4 would otherwise be licensed to exceed it by 40%.
	 * Every preview_scale is 1.0 today, so this is currently a distinction without
	 * a difference; it is written this way so it stays right when one is not.
	 *
	 * The fallback is the shipped 4.75 rather than a large number, so a browser
	 * that never received the inline property still caps. photoSlide() publishes
	 * 9999 for an uncapped calibration — see the note where it is written.
	 */
	font-size: min(
		calc(min(
			var(--paj-eng-photo-w, 13) * 1cqw * var(--paj-eng-adv-margin, 0.94) / (var(--paj-eng-len, 5) * (var(--paj-eng-adv, 0.62) + 0.02)),
			var(--paj-eng-photo-h, 8.4) * 1cqw * var(--paj-eng-adv-margin, 0.94) / var(--paj-eng-box, 1.35)
		) * var(--paj-eng-scale, 1)),
		calc(var(--paj-eng-photo-max, 4.75) * 1cqw)
	);
}

.paj-eng-photo[data-paj-eng-photo-state='named'] .paj-eng-photo__mark::before {
	content: var(--paj-eng-name, '');
}

/*
 * 🔴 THE STANDARD STATE DRAWS NOTHING HERE, AND MUST NOT (0.9.27, D31).
 *
 * There used to be two rules at this spot: a paint block and
 * `…[state='standard'] .paj-eng-photo__mark::before { content: 'PAJ GPS' }`,
 * which drew the wordmark in grey Roboto at 2.1cqw. PAJ's call of 2026-08-11
 * still stands — an un-printed tracker should look like a product rather than an
 * unfinished one — but it is now satisfied by PIXELS instead of by CSS: the
 * photo the server ships (products.json gallery_photo.file) has the real PAJ
 * wordmark printed into the housing, and the browser stacks the blank twin over
 * it for the named state. Only the MECHANISM changed.
 *
 * Putting either rule back paints synthetic grey text on top of real artwork —
 * two wordmarks on one device. This is the same defect strings-de.json's
 * _card_standard_logo_note already records for the buy-box card, one surface
 * later: "Restoring this string would print the logo TWICE on the same device."
 *
 * The .paj-eng-photo__mark element itself STAYS in the DOM in both states,
 * positioned and sized. It simply has no content in standard. That is what keeps
 * the toggle a single attribute flip and photoSlide's build idempotent.
 *
 * ⚠️ The flat-plate fallback surface KEEPS its wordmark — see
 * `.paj-eng-plate[data-paj-eng-plate-state='standard']` above. plateSlide runs
 * only when photoFull is empty, and it draws onto
 * paj-pet-finder-4g-mini-gps-tracker-gravur.webp, the BLANK twin, which has no
 * printed wordmark. Its CSS wordmark is the only one that surface has; deleting
 * it too would leave the fallback slide showing an unbranded device.
 */

/*
 * The blank-housing twin, mounted by photoSlide() and cross-faded to on the
 * named state.
 *
 * position/top/left are RESTATED, not inherited, and that is deliberate. On the
 * live page the snippet's `#pajGallery_<id> … img` rule (specificity 1,2,1)
 * already says exactly this and would win anyway — but dev/ui-test-build.php's
 * gallery fixture carries no snippet CSS at all, so without these three the
 * second image would flow BELOW the first and be clipped by the wrapper's
 * overflow:hidden. The suite would stay green while the picture was wrong. Same
 * values as the snippet's, so the two can never disagree about which wins.
 *
 * opacity, NOT display:none, for two reasons and both are load-bearing: that
 * same ID rule sets display:block at (1,2,1), so a display toggle would need an
 * !important to beat it; and a display:none image may not be decoded until it is
 * shown, which puts a fetch-and-decode flash on the first toggle — landing
 * exactly on the click that decides the sale. An opacity:0 image stays painted,
 * so the swap is instant. Nothing else in either stylesheet sets opacity on
 * these elements, so (0,2,0) is enough and the (0,2,1) floor is respected.
 */
.paj-eng-photo .paj-eng-photo__named {
	position: absolute;
	top: 0;
	left: 0;
	opacity: 0;
}

.paj-eng-photo[data-paj-eng-photo-state='named'] .paj-eng-photo__named {
	opacity: 1;
}

/* .paj-eng-photo__shimmer removed 2026-08-11 (0.9.4) with the remount in
   engraving.js that mounted it. 🔴 Do not reintroduce a sweep on the photo. 0.9.1
   had already had to feather this one with a mask and halve its alpha to stop it
   drawing a hard-edged rectangle of light across the bare housing, and the note it
   carried ended "if you remove the mask, remove the sweep with it" — this is that.
   PAJ asked for no light on the background anywhere; the buy-box glare went in the
   same change, and pajEngShimmer with it. See D14 and the note at the glare. */

/* 'Live-Vorschau'. JS gates the attribute on the REAL typed name — the
   'Buddy' placeholder must never be advertised as a live preview. */
.paj-eng-photo__pill {
	display: none;
	position: absolute;
	top: 12px;
	left: 12px;
	background: rgba(255, 255, 255, 0.92);
	border-radius: 999px;
	padding: 6px 14px;
	font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
	font-size: 13px;
	font-weight: 500;
	color: #1a2839;
}

.paj-eng-photo[data-paj-eng-pill='1'] .paj-eng-photo__pill {
	display: block;
}

/* No container-query support: viewport approximations, degraded but legible,
   same trade as the plate fallback above. */
@supports not (container-type: inline-size) {
	.paj-eng-photo {
		container-type: normal;
	}
	/* No .paj-eng-photo__mark paint rule here on purpose: the px transcription
	   of the plate used to live at this spot and would put the fill back on
	   every browser without container queries. Sizes only. */
	.paj-eng-photo[data-paj-eng-photo-state='named'] .paj-eng-photo__mark {
		/* Same width ratio as .paj-eng-plate__mark above, for the same reason and
		   with the same cap at 1, and the numerator tracks the fallback for the
		   same reason again — see the note over that @supports block (D26, D27).
		   D34's ceiling needs nothing here: min(2.6vw, 16px) already IS an absolute
		   cap on the drawn size, so this block was never able to grow a short name
		   the way the container-query formula could. It tracks the arm above in
		   intent, and it would only need the number if that 16px ever moved. */
		font-size: calc(min(2.6vw, 16px) * var(--paj-eng-scale, 1) * min(1, 6.5 / var(--paj-eng-len, 5)) * min(1, 0.62 / var(--paj-eng-adv, 0.62)));
	}
	/* The standard-state size rule that used to sit here went with the wordmark
	   in 0.9.27 (D31) — it was the px transcription of a block that no longer
	   exists, and sizing type that has no content is how a deleted rule comes
	   back. The stacked twin needs nothing here: opacity and absolute
	   positioning do not depend on container queries. */
}

/* No reduced-motion block down here any more: it existed only to cancel
   .paj-eng-photo__shimmer's animation, and nothing in the gallery animates now.
   For whoever animates something here next — the root-scoped block (.paj-eng *)
   still cannot reach this wrapper, because it lives in the gallery, outside the
   shortcode root. It will need its own block again. */

/* .paj-eng-price-inject removed 2026-08-10 with the injected breakdown line:
   the main price now swaps to the composed total instead (see the price
   integration in engraving.js). .paj-eng-price-total needs no rule — it sits
   inside the widget's p.price and inherits its styling.

   .paj-eng-chip-swapped removed 2026-08-06 with the deliveryChip integration
   that applied it. See the note where that integration was, in engraving.js.
   .paj-eng-chip-warn below stays: returnsChip still uses it, and is still
   gated on legal sign-off rather than removed. */

.elementor-icon-list-item.paj-eng-chip-warn {
	background: #fdebd2 !important;
}

.elementor-icon-list-item.paj-eng-chip-warn .elementor-icon-list-text {
	color: #8a4a12 !important;
}
