/*
 * eSIM Store — Theme Bridge
 *
 * Styles for classes used in theme patterns/templates that are NOT in
 * components.css or pages.css. These were authored by the previous Rune
 * session when scaffolding the theme (patterns/*.php, parts/*.html).
 *
 * The design HTML files use class names like .es-hero-grid and .hp-hero-grid
 * interchangeably in different contexts. This file bridges the gap so both
 * class naming conventions work side-by-side without modifying patterns.
 *
 * Also houses: country tile layout bridge, hero-homepage bridge,
 * WC template overrides, cart drawer, skip-link.
 *
 * Load order: tokens → components → pages → theme-bridge (last wins).
 */

/* ─── Skip Link (accessibility) ─────────────────────────── */
.skip-link {
  position: absolute;
  top: -100%;
  left: 1rem;
  z-index: 999;
  padding: 8px 16px;
  background: var(--color-contrast);
  color: var(--color-base);
  font-weight: 600;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
  text-decoration: none;
  font-size: 14px;
}
.skip-link:focus { top: 0; }

/* ─── WP / FSE body wrapper ──────────────────────────────── */
.wp-site-blocks { background: var(--color-base); min-height: 100vh; }

/* ─── Hero alias layer — DELETED IN ESM-70 (ADR-011) ─────────────────────────
 * Nineteen rules headed "theme pattern class aliases", opening with the claim
 * "Patterns use .es-hero-grid / .es-hero-content / .es-hero-h1 / .es-hero-sub".
 * They never did: patterns/hero-homepage.php emits the mock's `.hp-*` homepage
 * names, plus `.es-search` / `.es-globe` / `.es-live-indicator` — all alive, in
 * pages.css and components.css. This was one side of a rename that never
 * happened. Note the trap it set: `.es-search-wrap` and `.es-search` differ by
 * six characters and only the second one has ever styled anything. */

/* ─── Region chip (used in country-tile.php sidebar) ────── */
.es-tile-region-chip { display: inline-flex; align-items: center; font-size: 10.5px; font-weight: 700; font-family: var(--font-family-mono); letter-spacing: 0.06em; text-transform: uppercase; padding: 3px 8px; border-radius: var(--radius-sm); background: rgba(255,255,255,0.55); }
.es-region-local    { background: var(--color-info-soft); color: var(--color-info); }
.es-region-europe   { background: var(--color-region-europe-soft); color: var(--color-region-europe); }
.es-region-asia     { background: var(--color-region-asia-soft); color: var(--color-region-asia); }
.es-region-americas { background: var(--color-region-americas-soft); color: var(--color-region-americas); }
.es-region-mena     { background: var(--color-region-mena-soft); color: var(--color-region-mena); }
.es-region-africa   { background: var(--color-region-africa-soft); color: var(--color-region-africa); }
.es-region-oceania  { background: var(--color-region-oceania-soft); color: var(--color-region-oceania); }
.es-region-global   { background: var(--color-region-global-soft); color: var(--color-region-global); }

/* ─── Country tile — theme pattern bridge ────────────────── */
/*
 * Patterns use .es-country-tile / .es-tile-header / .es-tile-body / .es-tile-footer
 * components.css defines .es-tile (the base), .es-tile-inner, .es-tile-top, etc.
 * Bridge: make .es-country-tile behave like .es-tile, alias sub-elements.
 */
.es-country-tile {
  display: block;
  text-decoration: none;
  color: inherit;
  padding: 16px;
  border-radius: var(--radius-lg);
  aspect-ratio: 5/4;
  position: relative;
  /* ESM-90 — `overflow: hidden` was here, and it is the rule that let a tile be
   * SHORTER THAN ITS OWN CONTENT.
   *
   * A grid item's automatic minimum size (`min-height: auto`, the default)
   * resolves to the content-based minimum — but only while the item's overflow
   * is `visible`. Any other value makes it resolve to ZERO. So `overflow:
   * hidden` did not merely clip the spill; it authorised it, by telling the grid
   * that this box was happy at any height the aspect ratio asked for.
   *
   * Measured, because two plausible fixes are no-ops here and would have been
   * committed as if they worked: `min-height: fit-content` and
   * `min-height: max-content` both move the rendered height by exactly 0px on
   * this box (173 tiles × 5 viewports, unchanged to the pixel), and so does
   * `aspect-ratio: auto` on its own. `overflow: visible` is the one that works,
   * and it works because it restores the mechanism the other three were trying
   * to talk to. Verified across 1 038 tile renders at 320/375/414/768/1024/1440:
   * zero elements leaving the tile box, zero prices split mid-number.
   *
   * The clip protected nothing. Nothing in this tile is positioned, nothing
   * exceeds the padding box, and the flag carries its own border-radius — the
   * rounded corners had no content to round. What it did do was hide the fact
   * that the design's 5:4.5 proportion does not hold at 375: measured in the
   * DESIGN FILE ITSELF, destinations.html at 375 overflows its tile on 24 of 24
   * tiles, with four spilling visibly. The mock never resolved this width, so
   * the proportion is kept as a FLOOR (it still governs everywhere the content
   * fits — 768 and 1440 are unchanged) and the content decides the rest. */
  overflow: visible;
  transition: transform var(--transition-duration) var(--transition-easing-soft), box-shadow var(--transition-duration);
  cursor: pointer;
}
.es-country-tile:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }

/* ESM-90 — `flex-wrap: wrap` for the 13 tiles of 173 that carry a region chip
 * AND a per-GB chip. Flag 36 + chip up to 71.5 + per-GB up to 74.7 + two 8px
 * gaps is 198.2px against the 128.5px a tile has at 375: they cannot share one
 * line, and without wrapping the chip would overflow horizontally instead. With
 * it, the per-GB chip drops to a second line and the tile grows to hold it —
 * which it now can, see the `overflow` note above. The other 160 tiles are
 * unaffected: 36 + 8 + 74.7 = 118.7 fits. */
.es-tile-header { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: flex-start; gap: 8px; flex: 0 0 auto; }
/* `height: 100%` on the body pushed the footer (price row) out of the tile's
 * `overflow: hidden` box — the name/validity/price were clipped mid-line on every
 * catalog tile. The design's `.es-tile-inner` is a space-between column instead. */
.es-tile-body { display: flex; flex-direction: column; justify-content: flex-end; flex: 1 1 auto; min-height: 0; }

/* When used with .es-country-tile the inner layout is columnar */
.es-country-tile .es-tile-name {
  font-family: var(--font-family-heading);
  font-weight: 600;
  font-size: 20px;
  letter-spacing: -0.02em;
  line-height: 1.1;
}
/* ESM-49 — see the note on `.es-tile-price` in components.css. Two opacities
   were stacked here: 0.78 on the wrapper × 0.75 on the "from" label MULTIPLY to
   0.585, which put the label at 2.73:1 on the warm-neutral tint. The wrapper
   now carries the single tint-safe value and the label resets to 1 (i.e.
   inherits the wrapper's 0.9); size and weight keep the hierarchy.

   ESM-90 — `white-space: nowrap` on these two elements lives in components.css
   and is deliberately NOT restated here. These rules are more specific (0,2,0
   against 0,1,0) and load second, so anyone adding a `white-space` here would
   silently win and could unwrap the guarantee that a price never splits inside
   its own number. If you need to touch the price's box, touch it there. */
.es-country-tile .es-tile-price { font-size: 12.5px; opacity: 0.9; }
.es-country-tile .es-tile-price strong { display: block; font-family: var(--font-family-heading); font-weight: 700; font-size: 20px; opacity: 1; letter-spacing: -0.02em; }
.es-country-tile .es-tile-from { font-size: 11px; opacity: 1; }
.es-tile-per-gb { font-family: var(--font-family-mono); font-size: 11px; padding: 4px 8px; border-radius: var(--radius-full); background: rgba(255,255,255,0.55); white-space: nowrap; }
/* ESM-89: `.es-tile-flag { font-size: 28px; line-height: 1 }` was here and is
 * now in components.css, alongside the rest of that element's box. Two rules for
 * one element in two files at equal specificity is a trap: this file loads
 * second, so this `line-height: 1` would have silently beaten the 24px line box
 * components.css sets to keep the mobile tile from shifting. */
.es-tile-footer { display: flex; justify-content: space-between; align-items: flex-end; gap: 8px; margin-top: auto; padding-top: 8px; }

/* Country tile grid on popular-destinations homepage section */
.es-country-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; }
/* ESM-70: an empty `.es-country-grid-popular { }` ruleset sat here and read like
   the place the popular grid is configured. It is not — the rule that makes the
   homepage tiles square is `.es-country-grid-popular .es-country-tile
   { aspect-ratio: 1 }`, ~300 lines below under "Home: popular-destination
   tiles". */
@media (max-width: 1023.98px) { .es-country-grid { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 767.98px)  { .es-country-grid { grid-template-columns: repeat(2, 1fr); } }

/* ESM-70: `.es-tile-placeholder` deleted — a bootstrap state ("shown when no WC
   products yet") from before the importer ran, emitted by no pattern. The real
   empty state is `.es-empty-state`. */

/* ─── Trust bar — theme pattern bridge ───────────────────── */
/*
 * Pattern: trust-bar.php uses .es-trust-bar / .es-trust-item / .es-trust-icon-wrap
 * The mock's counterparts were `.hp-trust-item` / `.hp-trust-icon` in pages.css;
 * ESM-70 deleted those, since nothing rendered them. The measurements below are
 * still the mock's, read off index.html.
 *
 * ESM-9 — the items are now DIRECT children of `.es-trust-bar`, mirroring the
 * mock's `.hp-trustline`. They used to be wrapped in an extra `.es-trust-items`
 * flex box whose own wrapping put the row on two lines, so the bar measured
 * 132px where the design says 94 (32 + a 28px icon row + 32). `gap: 12px`
 * matches the mock's second, overriding `.hp-trustline` rule.
 * src: index.html .hp-trustline 1168x94 pad 32/0; .hp-trust-item h=28;
 *      .hp-trust-icon 28x28.
 */
.es-trust-bar { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 32px 0; border-top: 1px solid var(--color-neutral); border-bottom: 1px solid var(--color-neutral); flex-wrap: wrap; }
.es-trust-item { display: flex; align-items: center; gap: 10px; font-size: 14px; color: var(--color-contrast-muted); }
.es-trust-item strong { color: var(--color-contrast); font-weight: 600; }
.es-trust-icon-wrap { width: 28px; height: 28px; flex: 0 0 28px; border-radius: var(--radius-md); background: var(--color-accent-1-soft); color: var(--color-accent-1-hover); display: grid; place-items: center; }

/* ─── Cart panel ──────────────────────────────────────────
 * ESM-50 — the theme's own off-canvas drawer (`.es-cart-drawer`,
 * `.es-cart-backdrop`, `.es-cart-header/-title/-close/-body/-footer`) was
 * deleted along with parts/cart-drawer.html and its JS. It opened at the same
 * time as WooCommerce's, 400px wide under WC's 480px at a lower z-index, empty,
 * and half its class names had no rule in this file at all. The cart panel on
 * this site is WooCommerce's mini-cart drawer — the one the visitor has
 * actually been using — and the rules below are what the theme contributes to
 * it. Nothing else in the theme styles a cart panel; if a rule looks orphaned,
 * it is.
 * ───────────────────────────────────────────────────────── */

/* ESM-37 — WooCommerce's OWN mini-cart drawer has the same defect as ours had.
 * `mini-cart.css` closes it with `--is-hidden { opacity: 0; pointer-events: none }`
 * on an element it also marks `aria-hidden="true"` — neither of which removes a
 * descendant from the sequential focus order, so "Close" and "Start shopping"
 * stayed tabbable at x=1868 and x=1600 after our own drawer was made inert
 * (measured on / and /my-account/ at 1440).
 * `visibility: hidden` is the smallest thing that takes them out; it costs
 * nothing visually because WC already forces `transition: none !important` on
 * this overlay, so there is no fade to preserve, and the rule lifts the instant
 * WooCommerce drops `--is-hidden`. Hiding WC-rendered DOM with CSS rather than
 * removing it is the rule from Standards/wc-blocks-gotchas.md §Hydration & DOM. */
.wc-block-components-drawer__screen-overlay--is-hidden { visibility: hidden; }

/* The drawer line item repeats the price twice — `.wc-block-cart-item__prices`
   (unit) and `.wc-block-cart-item__total` (line), identical at quantity 1, one
   under the other. The /cart/ page already suppresses the first
   (`.es-cart-page table.wc-block-cart-items … __prices { display: none }`, see
   "CART LINE ITEMS" below); the drawer is the same component and was simply not
   covered, so it kept showing "$2.40 / $2.40". Scoped to the mini-cart so the
   /cart/ rule keeps owning /cart/.

   The long selector is the point, not clutter. `mini-cart-contents.css` sets
   `display: flex` at `table.wc-block-cart-items .wc-block-cart-items__row
   .wc-block-cart-item__prices` — 0,3,1. The obvious
   `.wc-block-mini-cart__drawer .wc-block-cart-item__prices` is 0,2,0 and loses
   silently; it was written that way first and measured still-visible
   (display:flex, 37x18 @x=979) on the deployed drawer. Restating WC's own
   scope with the drawer class in front makes it 0,4,1. This is the exact trap
   recorded in the site's CLAUDE.md — "WooCommerce scopes cart-row rules at
   0,3,1; an obvious override at 0,2,0 loses and looks like a stale cache". */
.wc-block-mini-cart__drawer table.wc-block-cart-items .wc-block-cart-items__row .wc-block-cart-item__prices { display: none; }

/* ─── Header ─────────────────────────────────────────────── */
/* Makes the FSE template-part wrapper itself sticky. (It used to claim it
   "supplements the .es-hdr from components.css"; that block styled nothing and
   was deleted in ESM-70 — the header's rules are under "Site header" below.) */
.wp-block-template-part[data-block="main-esim/header"] { position: sticky; top: 0; z-index: var(--z-header-sticky); }

/* ─── WooCommerce layout overrides ──────────────────────── */
/* Ensure WC blocks inherit the design system base styles */
.wc-block-grid .wc-block-grid__products { gap: 14px; }
.wc-block-components-price-slider__range-input-progress { background: var(--color-accent-1); }
.wc-block-components-checkbox input[type="checkbox"]:checked { accent-color: var(--color-accent-1); }

/* WC notice styles — align with design system */
.woocommerce-error,
.woocommerce-info,
.woocommerce-message {
  border-radius: var(--radius-md);
  font-size: 14.5px;
  padding: 14px 18px;
  margin-bottom: 16px;
}
.woocommerce-message { background: var(--color-success-soft); color: #1F5740; border-left: 3px solid var(--color-success); }
.woocommerce-info    { background: var(--color-info-soft);    color: #1F3F6B; border-left: 3px solid var(--color-info); }
.woocommerce-error   { background: var(--color-error-soft);   color: #7A1E10; border-left: 3px solid var(--color-error); }

/* ─── Region-card alias layer — DELETED IN ESM-70 (ADR-011) ──────────────────
 * Seventeen rules introduced by "region-card.php uses .es-region-grid /
 * .es-rc-* classes." It does not, and git says it never did — the pattern
 * renders `.hp-regional` / `.hp-region*` (pages.css). Gone with them: seven
 * `.es-region-card--{slug}` gradients, a third copy of a set pages.css already
 * keys to the same slugs. Do not confuse any of this with `.es-region-{slug}`
 * a few blocks above, which is live and tints the region chip. */

/* ─── Responsive globals ─────────────────────────────────── */
@media (max-width: 767.98px) {
  .es-trust-bar { flex-direction: column; align-items: flex-start; gap: 16px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THEME-PATTERN BRIDGE — built out 2026-07-29 (ESM-16 / KNOWN-RED R1)
   ═══════════════════════════════════════════════════════════════════════════
   Everything below carries the THEME's own `.es-*` vocabulary — the class names
   patterns/*.php, parts/*.html and templates/*.html actually emit — with values
   measured off the design mocks.

   ESM-70 UPDATE (ADR-011). This paragraph used to say the rules below "map the
   theme's vocabulary onto the DESIGN's rules, which live under the mock's names
   in components.css (`.es-hdr`, `.es-plan`, `.es-chip`, …) and pages.css
   (`.hp-*`, `.pd-*`, `.ds-*`, `.ct-*`, `.co-*`, `.me-*`, `.oc-*`)". Two thirds
   of those mock names never matched an element and are now deleted. Nothing
   below "maps onto" a rule in another file any more — these ARE the rules, and a
   value here can no longer be assumed to inherit from a mock-named base: if a
   property is missing, it is missing.

   Before this block the theme's vocabulary had ZERO rules — `.es-plan-card`,
   `.es-pdp-hero`, `.es-catalog-sidebar`, `.es-site-header`, `.es-pc-price`,
   `.es-my-account` matched nothing in any of the four stylesheets, so the
   country PDP rendered essentially unstyled (h1 at 16px, plan cards 89px tall
   with padding 0, coverage a bare <ul> with the UA's 40px indent).

   RULES OF THIS FILE
   · Values come from the design mocks in websites/esim-store/deliverables/design,
     measured in Chromium at 1440/768/375 (Playwright, dsf=1, --hide-scrollbars).
     Each block cites its source as `src: <mock> <selector> <measurement>`.
   · No raw hex. Every colour goes through a tokens.css custom property.
   · Loaded LAST from global.css, so these rules win over components/pages.
   ═════════════════════════════════════════════════════════════════════════ */

/* ─── Site header ─────────────────────────────────────────────────────────
 * src: index.html .es-hdr rect 1440x92.5, .es-hdr-inner rect 1280x91.5
 *      @x=80, padding 14px 56px, max-width 1280, gap 32.
 * The mock's 92.5 comes out of a nav label that happens to wrap to two lines at
 * 1440. We pin the bar height explicitly instead of reproducing that accident,
 * so the chrome is the same height on every page regardless of menu labels.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header-sticky);
  background: rgba(251, 247, 240, 0.85);
  backdrop-filter: saturate(140%) blur(12px);
  border-bottom: 1px solid transparent;
}
.es-site-header.is-scrolled { border-bottom-color: var(--color-neutral); }

/* The mobile menu overlay must escape the header's box.
 *
 * `backdrop-filter` above makes `.es-site-header` a CONTAINING BLOCK for
 * `position: fixed` descendants (CSS Filter Effects §containing block, same rule
 * as `transform`). `core/navigation`'s overlay is `position: fixed; inset: 0`
 * and lives inside the header, so `inset: 0` resolved to the header's 93px bar
 * instead of the viewport: measured at 375, the opened menu was a 375×92 white
 * strip with the nav list laid out at y=51…243 — three of the four links were
 * outside their own overlay and unreachable, and the drawer looked blank.
 *
 * Found while adding the drawer search (ESM-54); the CONTROL — same page with
 * the search form removed from the DOM, i.e. the exact pre-existing markup —
 * reproduced the identical 92px clip, so the overlay was already broken.
 *
 * Core sets `has-modal-open` on <html> for exactly the duration the overlay is
 * open, so the glass effect is dropped only while a full-screen menu covers the
 * page — there is nothing left to see through it. The sticky-header look at
 * every other moment is untouched. */
html.has-modal-open .es-site-header {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

/* ─── ESM-102: THE HEADER BAR HOLDS FIVE ITEMS ────────────────────────────
 *
 * THREE VALUES BELOW LEFT THE MOCK ON PURPOSE. They are documented together
 * rather than one comment each, because separately none of them is enough and
 * separately none of them is defensible — the decision is the set.
 *
 *     what                          mock (src:)      now     ─┐
 *     .es-header-inner gap          32               16       │ a fifth top-level
 *     nav item horizontal padding   14               10       ├─ nav item ("About",
 *     header search pill            min-width 240    width 224 │ ESM-99) does not
 *                                                            ─┘ otherwise fit
 *
 * HOW WRAP IS MEASURED. The header wraps to two rows at and below a threshold
 * width; the threshold is found by bisecting the viewport while reading
 * `.es-header-inner`'s rendered height, on the deployed page, with the fifth item
 * cloned into the live DOM. NOT by reading `flex-wrap`: that is `wrap` at every
 * width by design (the mobile-overflow note further down), so it would report
 * "wrapped" everywhere and prove nothing. Measured on the deployed 0.17.0,
 * 2026-08-25 — esm102-header-verify.mjs, esm102-finalists.mjs.
 *
 * WHAT IT COSTS AND WHAT IT RETURNS. Each row below is measured, and each adds
 * one value to the row above it, so the arithmetic is visible:
 *
 *                                                wraps at   vs today   returns
 *     4 items, mock values — what 0.17.0 shipped    ≤1160        —          —
 *     5 items, mock values                          ≤1235      +75          —
 *       + inner gap 32→16                           ≤1203      +43        32px
 *       + nav padding 14→10                         ≤1163       +3        40px
 *       + pill 240→224   ← what ships here          ≤1147      −13        16px
 *
 * The fifth item costs 75px of headroom; the three values return 88px. So the
 * band in which this header is broken ends up NARROWER than it is today while
 * gaining the item — which is the entire justification. Adding the item at the
 * mock's values would have knowingly widened an existing defect by 75px, and
 * note the third row: gap and padding together still land 3px worse than today.
 * There is no two-value version of this.
 *
 * WHY THIS ORDER. The values are spent cheapest-to-look-at first, and "cheap" is
 * measured, not asserted:
 *
 * · THE GAP IS INVISIBLE AT DESKTOP, so it is spent hardest. The bar is
 *   `justify-content: space-between`, so what a visitor sees between the logo,
 *   the nav and the actions is distributed FREE SPACE; `gap` is only a minimum
 *   that binds once the free space is gone. Measured with five items at both 32
 *   and 16, the rendered distance between the groups is IDENTICAL — 138.3px at
 *   1440, 125.3 at 1366, 82.3 at 1280, 42.3 at 1200 — and the two values first
 *   differ at 1160, which is inside the band being fixed (esm102-gap-visibility).
 * · The nav padding sets the hover pill and the label rhythm, so it moves once,
 *   by 4px.
 * · The pill is the most visible single element in the bar and it has a HARD
 *   FLOOR (below), so it is spent last and least.
 *
 * WHAT THIS DOES NOT FIX. The header still wraps below ~1147, and the 1024–1160
 * band — laptops — is still wrong. Spacing cannot close it: the only measured way
 * out is raising the nav→drawer breakpoint above 1024, which is a decision about
 * which items stop being top-level, i.e. information architecture. Filed
 * separately. Do NOT close it by shaving further off these three: the gap is
 * already past the point where it is free, and the pill is 4px off its floor.
 *
 * THE PILL CHANGED PROPERTY, `min-width: 240` → `width: 224`, AND THAT IS THE
 * NON-OBVIOUS PART. Lowering the floor does not lower the pill: with any
 * `min-width` under 217 it renders at exactly 217, because the size then comes
 * from the input's UA-DEFAULT INTRINSIC WIDTH (`size` is unset → 20 characters)
 * rather than from us. Proved by emptying the placeholder and by tripling it —
 * both leave 217 untouched — while halving `size` moves it to 147. Handing this
 * header's geometry to a UA default and a font metric is what the frozen
 * `header-inner-w` expectation exists to prevent, so the width is ours now.
 *
 * 224 IS FOUR PIXELS ABOVE A MEASURED FLOOR OF 220, AND THE FLOOR IS THE
 * PLACEHOLDER. "Search destinations…" stops rendering whole below it: scanning
 * the pill in 2px steps and reading the field's own overflow, 218 cuts 1px, 216
 * cuts 3, 212 cuts 7. It is visible — at 212 the pill reads "Search
 * destinations." with the ellipsis reduced to a single dot, photographed at 4×
 * (esm102-ph-212.png vs esm102-ph-220.png).
 *
 * READ THIS BEFORE MEASURING TEXT AGAIN. The first version of this decision
 * shipped 212 on a floor of 206.2 computed from `canvas.measureText()` with a
 * font shorthand rebuilt from the input's computed style. It was wrong, and it
 * was wrong in the dangerous direction: a plausible number, 14px too small, that
 * turned into a clipped placeholder on every page of the site. Canvas text
 * metrics are not the layout the page performs — the same family as measuring an
 * emoji's ink with font metrics (the flag note in components.css). Measure a
 * string by laying it out, or by reading the element's own overflow, and then
 * look at it.
 *
 * IF THE PLACEHOLDER COPY EVER CHANGES, 224 HAS TO BE RE-MEASURED before the
 * copy ships. It is the one number here that a text edit can invalidate.
 *
 * THE ITEM'S LABEL IS LOAD-BEARING TOO, WHICH IS WHY IT IS "About" AND NOT
 * "About us" like the footer's Company row. The longer label is an 80.8px item
 * instead of 62.7px and lands the header at ≤1165px — WORSE than the ≤1160 it
 * has to beat, i.e. the whole change would be undone by two extra characters.
 * Measured, esm102-label-cost.mjs. The two labels naming one page is a deliberate
 * cost, not an oversight.
 *
 * TOUCH TARGETS ARE UNAFFECTED, MEASURED. Every item loses 8px of width and no
 * height: the smallest target goes 57.5×41.8 → 49.5×41.8 ("FAQ") and the new
 * item is 62.7×41.8, all far above the 24×24 of WCAG 2.2 SC 2.5.8 — whose
 * spacing exception is not needed either, because the 4px between adjacent
 * targets is untouched (`.wp-block-navigation__container` gap is not one of the
 * three values).
 *
 * NOTHING HERE IS ON SCREEN ON A PHONE. The drawer below 1024 overrides this
 * padding to `10px 0` with full-width rows at `--touch-target`, and
 * `.es-header-search` is hidden there. Verified rather than reasoned:
 * `.es-header-inner` is 91.5px tall at 375 / 414 / 768 / 900 / 1023 both before
 * and after, i.e. this header never wraps on a phone, so the row-gap half of
 * `gap: 16` is inert there too.
 * ────────────────────────────────────────────────────────────────────────────── */

.es-header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;            /* ESM-102 — mock 32; see the block above. */
  width: 100%;
  max-width: var(--container-max-width);
  margin: 0 auto;
  padding: 14px var(--container-gutter);
  min-height: 91.5px;   /* src: index.html .es-hdr-inner h=91.5 */
}
.es-logo-wrap { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
.es-header-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }

/* Nav — src: index.html .es-nav gap 4, .es-nav-link 14.5px / pad 10px 14px. */
.es-main-nav .wp-block-navigation__container { gap: 4px; }
.es-main-nav .wp-block-navigation-item__content {
  font-size: 14.5px;
  font-weight: 500;
  color: var(--color-contrast);
  text-decoration: none;
  padding: 10px 10px;   /* ESM-102 — mock 10px 14px; see the block above. */
  border-radius: var(--radius-md);
  min-height: 40px;
  display: inline-flex;
  align-items: center;
  transition: background var(--transition-duration) var(--transition-easing);
}
.es-main-nav .wp-block-navigation-item__content:hover { background: rgba(15, 26, 46, 0.05); }

/* Header search — src: index.html .es-hdr-search 240x40.5, radius full. */
.es-header-search .wp-block-search__inside-wrapper {
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-full);
  padding: 4px 4px 4px 14px;
  width: 224px;         /* ESM-102 — mock `min-width: 240`; see the block above. */
  min-height: 40px;
  align-items: center;
}
.es-header-search .wp-block-search__input {
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 13.5px;
  outline: none;
  padding: 0;
  min-width: 0;
}
.es-header-search .wp-block-search__button { background: transparent; border: 0; padding: 6px 8px; color: var(--color-contrast-faint); }

/* Account link + mini-cart trigger — src: .es-hdr-account 40x40, radius md. */
.es-header-account a,
.es-header-minicart .wc-block-mini-cart__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 40px;
  min-height: 40px;
  padding: 8px 10px;
  border-radius: var(--radius-md);
  font-size: 14px;
  font-weight: 500;
  color: var(--color-contrast);
  text-decoration: none;
  background: transparent;
}
.es-header-account a:hover,
.es-header-minicart .wc-block-mini-cart__button:hover { background: rgba(15, 26, 46, 0.05); }

/* ─── Breadcrumb ──────────────────────────────────────────────────────────
 * src: cart.html / destinations.html / japan.html .es-crumbs
 *      rect 1168x51.5 — 13px text, line-height 19.5, padding 16px 0, gap 8.
 * components.css styles `.es-breadcrumb` only as an inline 13px line with a
 * bottom margin (it was written for the PDP hero), so the standalone trail on
 * cart/checkout/catalog had no vertical rhythm at all.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-breadcrumb {
  font-size: 13px;
  line-height: 1.5;
  color: var(--color-contrast-muted);
  padding: 16px 0;
  margin-bottom: 0;
}
.es-breadcrumb-list { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; list-style: none; margin: 0; padding: 0; }
.es-breadcrumb-sep { color: var(--color-contrast-faint); font-size: 13px; opacity: 1; padding: 0; }
.es-breadcrumb-current,
.es-breadcrumb-list [aria-current="page"] { color: var(--color-contrast); font-weight: 500; opacity: 1; }
/* Inside the PDP hero the trail keeps its own spacing (it sits above the flag). */
.es-pdp-hero .es-breadcrumb { padding: 16px 0; }
/* ESM-49: …and its own colour. The hero sets `color:{tint.fg}` inline from
   inc/tile-tint.php — the one foreground guaranteed ≥4.5:1 against that tint
   (5.90–10.41 across the 15). `--color-contrast-faint` is a token for NEUTRAL
   grounds only; on the Japan tint the separator measured 2.01:1 and on Spain's
   2.11:1. Inheriting is therefore not a shortcut, it is the correct pair. */
.es-pdp-hero .es-breadcrumb,
.es-pdp-hero .es-breadcrumb-sep { color: inherit; }

/* ─── Sections & section heads ────────────────────────────────────────────
 * src: index.html .hp-section-head rect 1168x116, margin-bottom 28, gap 16,
 *      h2 30px / line-height 33; p 15px muted.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-section-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 16px;
  margin-bottom: 28px;
  flex-wrap: wrap;
}
/* `align-items: flex-end` above is correct for the ROW variant (title block on
 * the left, "View all" button bottom-aligned on the right). On the homepage's
 * "Regional & global plans" head the block layout is `orientation: vertical`,
 * so core adds `flex-direction: column` — and on a column axis `flex-end` is
 * the RIGHT edge, which right-aligned that heading and its subtitle against
 * everything else on the page. Scope the cross-axis alignment to the direction
 * it was written for. */
.es-section-head.is-vertical { align-items: flex-start; }
.es-section-head h2 { font-size: 30px; font-weight: 600; letter-spacing: -0.03em; line-height: 1.1; }
.es-section-head p { color: var(--color-contrast-muted); margin: 6px 0 0; max-width: 480px; font-size: 15px; }

/* ─── Home: popular-destination tiles ─────────────────────────────────────
 * src: index.html .hp-grid-6 6 cols / gap 14 on the 1168 rail → tile 183x183;
 *      `.hp-grid-6 .es-tile { aspect-ratio: 1 }` — the HOME tile is square,
 *      unlike the catalog tile (5/4.5). Scope the override to the popular grid.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-country-grid-popular .es-country-tile { aspect-ratio: 1; }

/* ─── Catalog (/shop/) ────────────────────────────────────────────────────
 * src: destinations.html — .ds-head padding 28px 0 8px; .ds-grid
 *      `grid-template-columns: 280px 860px` gap 28 on the 1168 rail;
 *      .es-filters 280 wide / padding 22 / sticky 84; .ds-results-info 13.5px.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-catalog-hero { padding: 28px 0 8px; background: transparent; }
.es-catalog-hero h1 { line-height: 1.15; }
.es-catalog-hero p { color: var(--color-contrast-muted); margin: 6px 0 0; font-size: 15px; max-width: 620px; }

/* Segmented tab control — src: .es-tabs 321.3x47 pad 4; .es-tab 100.4x37. */
.es-catalog-tabs {
  display: inline-flex;
  padding: 4px;
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-full);
  margin-top: 28px;
  gap: 0;
}
.es-catalog-tab {
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  background: transparent;
  border: 0;
  padding: 8px 18px;
  border-radius: var(--radius-full);
  cursor: pointer;
  color: var(--color-contrast-muted);
  min-height: 36px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
}
.es-catalog-tab:hover { color: var(--color-contrast); }
.es-catalog-tab.is-active,
.es-catalog-tab[aria-selected="true"] { background: var(--color-contrast); color: var(--color-base); font-weight: 600; }

/* Two-column body — grid, not wp:columns' flex, so the 280px rail is exact. */
.es-catalog-body { padding-bottom: 80px; }
.es-catalog-layout.wp-block-columns {
  display: grid;
  grid-template-columns: 280px 1fr;
  gap: 28px;
  align-items: start;
}
.es-catalog-layout > .wp-block-column { flex-basis: auto !important; min-width: 0; }

.es-catalog-sidebar {
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-lg);
  padding: 22px;
  position: sticky;
  top: 84px;
}
.es-catalog-grid-col { min-width: 0; }

/* Filter groups — src: .es-filter-group padding 16px 0 + divider; first-child
 * has no top padding, last-child no divider/bottom padding. */
.es-catalog-filters { display: block; }
.es-filter-group { padding: 16px 0; border-bottom: 1px solid var(--color-neutral); }
.es-filter-group:first-child { padding-top: 0; }
.es-filter-group:last-child { border-bottom: 0; padding-bottom: 0; }
.es-filter-label {
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--color-contrast-faint);
  margin: 0 0 12px;
}
.es-filter-chips { display: flex; flex-wrap: wrap; gap: 6px; }

/* Filter chip — src: .es-chip rect 44.3x38, padding 6px 12px, 1px border,
 * radius full. The mock RENDERS the chip label at 16px only because
 * components.css declares `font-size: 13px` and then re-inherits it with a
 * later `font: inherit` shorthand — an accident, not an intent. We keep the
 * design's declared 13px and pin line-height 24 so the box is still exactly
 * 38px tall (24 + 2x6 padding + 2x1 border), which is what the gate asserts. */
.es-filter-chip {
  display: inline-flex;
  align-items: center;
  font-size: 13px;
  line-height: 24px;
  font-weight: 500;
  padding: 6px 12px;
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-full);
  background: transparent;
  color: var(--color-contrast);
  text-decoration: none;
  cursor: pointer;
}
.es-filter-chip:hover { background: var(--color-base); }
.es-filter-chip.is-active,
.es-filter-chip[aria-pressed="true"] { background: var(--color-contrast); border-color: var(--color-contrast); color: var(--color-base); }

/* ESM-39 — "Clear filters (N)". Rendered by patterns/catalog-filters.php only
 * while at least one filter is applied, so it can never be a control that does
 * nothing. Styled as a quiet link rather than a chip: it is an escape hatch, not
 * a fifth filter axis, and it must not read as another selectable value. */
.es-filter-reset-group { border-bottom: 0; }
.es-filter-reset {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  font-weight: 500;
  line-height: 24px;
  color: var(--color-contrast-muted);
  text-decoration: none;
}
.es-filter-reset:hover { color: var(--color-contrast); text-decoration: underline; text-underline-offset: 3px; }

/* Hotspot toggle — src: .es-toggle padding 6px 0; .es-toggle-switch 36x20.
 * The track was a bare <span> with no rule at all: computed width 0, i.e. an
 * INVISIBLE control whose <label> still occupied 202.8px of tap area.
 *
 * ESM-39 — the CONTROL is gone (patterns/catalog-filters.php explains why:
 * `_hotspot_supported` is `unknown` on every product, so "Hotspot supported
 * only" could never match anything, and the checkbox had no handler at all).
 * These rules are kept as an inert definition so the switch reappears styled,
 * not naked, the day Keepgo publishes the field and the control comes back —
 * which is exactly the failure this block was written to fix the first time. */
.es-filter-toggle { display: block; }
.es-toggle-label {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 0;
  font-size: 14px;
  cursor: pointer;
}
.es-toggle-text { order: 1; flex: 1; min-width: 0; }
.es-toggle-track {
  order: 2;
  flex: 0 0 auto;
  display: block;
  width: 36px;
  height: 20px;
  border-radius: var(--radius-full);
  background: var(--color-neutral-strong);
  position: relative;
  transition: background var(--transition-duration);
}
.es-toggle-track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--color-base-elevated);
  transition: transform var(--transition-duration);
}
/* Keep the checkbox focusable (never display:none) — WCAG keyboard access. */
.es-toggle-input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}
.es-toggle-input:checked ~ .es-toggle-track { background: var(--color-accent-1); }
.es-toggle-input:checked ~ .es-toggle-track::after { transform: translateX(16px); }
.es-toggle-input:focus-visible ~ .es-toggle-track { box-shadow: var(--shadow-focus); }

/* Results / sort bar — src: .ds-results-info 13.5px, margin-bottom 16. */
.es-catalog-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  margin-bottom: 16px;
  font-size: 13.5px;
  color: var(--color-contrast-muted);
}
.es-catalog-bar .woocommerce-result-count,
.es-catalog-bar .wc-block-product-results-count { margin: 0; font-size: 13.5px; }
.es-catalog-bar .wc-block-catalog-sorting select,
.es-catalog-bar .woocommerce-ordering select {
  font: inherit;
  font-size: 13.5px;
  padding: 8px 12px;
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-md);
  background: var(--color-base-elevated);
  color: var(--color-contrast);
  min-height: 40px;
  cursor: pointer;
}

/* Catalog product grid — src: .ds-countries 4 cols / gap 14 of the 860 column
 * → tile 204.5 wide; `.ds-countries .es-tile { aspect-ratio: 5/4.5 }` → 184. */
.es-catalog-grid-col .wc-block-product-template {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 14px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.es-catalog-grid-col .wc-block-product-template > li { margin: 0; }
.es-catalog-grid-col .es-country-tile { aspect-ratio: 5/4.5; }

/* ESM-97 — the region archive is the SAME catalog frame with a different card.
 *
 * /shop/ lists destinations (`.es-country-tile`, 4 across the 860 column);
 * /esim-region/<slug>/ lists PLANS (`.es-plan-card`), so it takes three across:
 * (860 − 2x14) / 3 = 277.3, against the design's ported plan card of 281.5
 * (japan.html `.es-plan`, 4 across the full 1168 rail). Four across the 860
 * column would be 204.5 — the DESTINATION tile's width — and would squeeze a
 * card whose CTA is a full-width 235.5px button.
 *
 * Before this change these cards sat at 376 wide (3 across the whole 1168 rail,
 * no sidebar) and 237.8 tall. Narrowing them to 277.3 will wrap one more
 * `.es-pc-tier` line on the labels that are long enough — the same 16.5px step
 * that already makes the country PDP's cards 254.3 rather than 237.8 (ESM-78).
 * `.es-pc-meta { margin-bottom: auto }` above is what keeps a row uniform when
 * that happens; it is not new here and it is why this narrowing does not
 * reintroduce the ragged-CTA defect.
 *
 * The breakpoints mirror `.es-section-plans` (2 columns, then 1) rather than
 * the destination grid's (3, then 2): a plan card carries a full-width CTA and
 * a per-GB figure, and stops being readable well before a flag tile does. */
.es-region-archive .es-catalog-grid-col .wc-block-product-template {
  grid-template-columns: repeat(3, 1fr);
}

@media (max-width: 1023.98px) {
  .es-catalog-layout.wp-block-columns { grid-template-columns: 1fr; }
  .es-catalog-sidebar { position: static; }
  .es-catalog-grid-col .wc-block-product-template { grid-template-columns: repeat(3, 1fr); }
  .es-region-archive .es-catalog-grid-col .wc-block-product-template { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 767.98px) {
  .es-catalog-grid-col .wc-block-product-template { grid-template-columns: repeat(2, 1fr); }
  .es-region-archive .es-catalog-grid-col .wc-block-product-template { grid-template-columns: 1fr; }
}

/* The tab strip is a single inline-flex row and, with a fourth tab on a place
 * region ("All destinations · Single country · Global · Europe"), it exceeds a
 * 375 viewport. It scrolls horizontally rather than wrapping: wrapping would
 * break the segmented pill into two stacked halves, which is a different
 * control. `-webkit-overflow-scrolling` is omitted deliberately — it is a
 * no-op on current iOS and lint noise everywhere else. */
@media (max-width: 767.98px) {
  .es-catalog-tabs {
    display: flex;
    max-width: 100%;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .es-catalog-tabs::-webkit-scrollbar { display: none; }
  .es-catalog-tab { flex: 0 0 auto; white-space: nowrap; }
}

/* ─── Country PDP hero ────────────────────────────────────────────────────
 * src: japan.html .pd-hero padding 8px 0 40px; .pd-hero-grid 1.2fr/1fr gap 56;
 *      .pd-flag-card 112x80 (padding 8, radius lg, shadow md);
 *      h1 64px / line-height 1 / margin-top 24;
 *      .pd-summary 3 cols gap 18 → .pd-summary-item 190.2x108.5, pad 16px 18px.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-pdp-hero { padding: 8px 0 40px; }

/* ESM-80 — the tinted band gets its own edges, the text keeps the rail.
 *
 * On /esim-country/gf/ the breadcrumb, flag, title and description all began at
 * x=136 — and so did the tinted panel, measured x=136 w=1168 @1440, x=32 w=704
 * @768, x=20 w=335 @375. Text flush against the edge of a coloured surface at
 * every width.
 *
 * The band is not in the design. japan.html's `.pd-hero` is a TRANSPARENT block
 * inside `.es-container` on the cream page — the tint is
 * `patterns/pdp-hero.php` painting `esim_get_country_tile_tint()` onto the
 * section. A surface was introduced without giving it an inset.
 *
 * THE OBVIOUS FIX IS THE WRONG ONE, and the gate says so. Adding
 * `padding-inline` to the panel would move the h1 off x=136 and break two
 * checks that are frozen on purpose: `pdp-rail-anchor` (the country name IS the
 * PDP's rail anchor) and `callout-on-rail` (the activation-timing callout is
 * asserted to align-left with that h1, and the callout sits OUTSIDE this panel,
 * so it would not move with it). The hero's text has to stay where it is.
 *
 * So the panel moves instead: `max-width: none` releases it from the constrained
 * layout's 1168 cap, the negative inline margin takes it out to the root gutter
 * on both sides, and an equal `padding-inline` puts the content back. Net effect
 * at 1440 — panel x=0 w=1440, content x=136, unchanged. The inset the text now
 * has from the panel edge is exactly the site gutter, at all three widths
 * (56 / 32 / 20), because it IS the site gutter.
 *
 * `.es-main >` is not decoration: WordPress's constrained-layout rule
 * (`.is-layout-constrained > :where(…)`) computes to (0,1,0), which a bare
 * `.es-pdp-hero` only ties with — and this file has been bitten by a load-order
 * tie twice already. The child combinator with the parent class makes it (0,2,0).
 *
 * Only the country archive renders this section: /esim-region/europe/ was
 * checked and has no `.es-pdp-hero` node at all (its template opens with a plain
 * h1 on the rail), so there is no second surface to carry the same fix. */
.es-main > .es-pdp-hero {
  max-width: none;
  /* `!important` is REQUIRED here, not preferred, and it is the only one in this
   * block. WordPress's constrained-layout rule ships
   *   `.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull))
   *    { max-width: …; margin-left: auto !important; margin-right: auto !important }`
   * — verified in the served global stylesheet, not assumed. An `!important`
   * declaration cannot be beaten by specificity, only by another `!important`,
   * which is why the first attempt at this fix silently did nothing: `max-width`
   * and `padding-inline` landed (measured pl=56px) while the margins stayed
   * `auto`, leaving the panel at x=56 w=1328 instead of x=0 w=1440.
   *
   * The documented alternative — adding `alignfull` to the section so core's
   * `:not(.alignfull)` stops matching it — was rejected on measurement: core
   * then applies `padding-inline: root-padding` to the alignfull's CHILDREN,
   * which lands on `.es-container` at a specificity that only TIES with the
   * theme's `padding-left: 0`, and if core won that tie the hero text would move
   * to x=192 and break the frozen rail anchor. A tie is not a fix. */
  margin-left: calc(var(--container-gutter) * -1) !important;
  margin-right: calc(var(--container-gutter) * -1) !important;
  padding-inline: var(--container-gutter);
}
.es-pdp-hero-inner { display: block; }
.es-pdp-hero-body {
  display: grid;
  grid-template-columns: 1.2fr 1fr;
  gap: 56px;
  align-items: center;
}
.es-pdp-hero-left { min-width: 0; }
/* ESM-91 — the flag CARD. Its own box is unchanged (112×80 including 8px of
 * padding, which is what the frozen `pdp-flag-wh` check asserts); what changed
 * is that it now holds a picture instead of a text character.
 *
 * src: japan.html `.pd-flag-card { padding: 8px }` + `.pd-flag-card img
 * { width: 96px; height: 64px; border-radius: var(--radius-md); object-fit:
 * cover }`. The theme had the card and not the picture: measured from rendered
 * pixels on the deployed page, the 44px glyph drew 40×28 inside a 112×80 card —
 * about a ninth of the area the design fills.
 *
 * `font-size` moves off the card and onto the glyph fallback below. On a card
 * whose content is an <img> it is a text metric with nothing to size, and
 * leaving it here would have made the card look like the place the flag's size
 * is decided when it no longer is. */
.es-pdp-flag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 112px;
  height: 80px;
  padding: 8px;
  background: var(--color-base-elevated);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}
.es-pdp-flag-img {
  display: block;
  width: 96px;
  height: 64px;
  border-radius: var(--radius-md);
  object-fit: cover;
}
/* The glyph, on the 13 destinations we host no flag for. Sized as text, as it
 * has to be — see inc/country-flag.php for which thirteen and why. */
.es-pdp-flag-glyph { font-size: 44px; line-height: 1; }
.es-pdp-country-name {
  font-size: clamp(2.5rem, 1.6rem + 3vw, 4rem);
  font-weight: 600;
  letter-spacing: -0.035em;
  line-height: 1;
  margin: 24px 0 0;
}
.es-pdp-hero-tags { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 16px; }
.es-region-chip {
  display: inline-flex;
  align-items: center;
  font-size: 10.5px;
  font-weight: 700;
  font-family: var(--font-family-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: var(--radius-sm);
  text-decoration: none;
  background: var(--color-neutral);
  color: var(--color-contrast);
}
.es-pdp-plan-count-chip {
  font-family: var(--font-family-mono);
  font-size: 12px;
  color: var(--color-contrast-muted);
  padding: 0 10px;
  border-radius: var(--radius-full);
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
}

/* ESM-81 — the two hero chips are one row, so they get one box.
 *
 * Measured on /esim-country/gf/ @1440 before the fix: "EUROPE" 57.6x21.8 at
 * y=363.1, "22 plans" 79x28 at y=360.0. Two different heights, two different
 * top edges, and — because the vertical padding differed as well (3px against
 * 4px on top of two different font sizes) — two different text baselines. The
 * row's `align-items: center` was centring two unequal boxes, which is why they
 * read as leaning against each other rather than sitting on a line.
 *
 * NOT a typeface problem, which is worth recording because it looks like one:
 * both chips are already JetBrains Mono (verified on the deployed page —
 * `font-family` resolves to "JetBrains Mono" on each). What differs is size
 * (10.5 vs 12), padding and radius.
 *
 * The fix pins ONE height and ONE line box on both and lets each keep its own
 * treatment — the region chip stays the design's soft-filled `.es-badge`
 * (japan.html: 11px mono uppercase, 3px 8px, radius sm), the plan count stays
 * the outlined pill. 22px is the design badge's own height (11px at
 * line-height 1.4 plus 3px each side = 21.4). With a fixed `line-height: 18px`
 * inside a centred 22px box, the two baselines land 0.35px apart — that is a
 * consequence of the differing font sizes and is below the rendering grid, not
 * a residual defect.
 *
 * `box-sizing` is restated because the plan chip carries a 1px border and the
 * region chip does not; without it the border would make the outlined chip 2px
 * taller than the filled one and reintroduce the whole finding. */
.es-pdp-hero-tags > .es-region-chip,
.es-pdp-hero-tags > .es-pdp-plan-count-chip {
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
  height: 22px;
  line-height: 18px;
}
.es-pdp-hero-desc {
  color: var(--color-contrast-muted);
  font-size: 17px;
  line-height: 1.55;
  margin: 22px 0 28px;
  max-width: 540px;
}
.es-pdp-hero-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.es-pdp-stat {
  padding: 16px 18px;
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-md);
}
.es-pdp-stat-val { display: block; font-family: var(--font-family-heading); font-size: 22px; font-weight: 700; letter-spacing: -0.025em; }
.es-pdp-stat-label { font-size: 12.5px; color: var(--color-contrast-muted); margin-top: 4px; display: block; }

@media (max-width: 1023.98px) {
  .es-pdp-hero-body { grid-template-columns: 1fr; }
}
@media (max-width: 767.98px) {
  .es-pdp-hero-stats { grid-template-columns: 1fr 1fr; }
}

/* ─── Activation-timing callout ───────────────────────────────────────────
 * src: japan.html .es-callout rect 1168x77.5, padding 16px 20px, radius lg.
 * The theme emits SINGLE-dash modifiers (`es-callout-warning`) and these are
 * the only definitions of them. The mock's double-dash set in components.css was
 * deleted in ESM-70 — nothing ever emitted it, so "alias both", as this note used
 * to read, aliased one real form and one imaginary one. `.es-callout`, `-icon`
 * and `-body` still come from components.css.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-callout-warning { background: var(--color-warning-soft); border-color: rgba(143, 94, 24, 0.25); color: #6A4710; }
.es-callout-warning .es-callout-icon { background: var(--color-warning); color: var(--color-base-elevated); }
.es-callout-info { background: var(--color-info-soft); border-color: rgba(47, 94, 158, 0.22); color: #1F3F6B; }
.es-callout-info .es-callout-icon { background: var(--color-info); color: var(--color-base-elevated); }
.es-callout-success { background: var(--color-success-soft); border-color: rgba(44, 118, 87, 0.22); color: #1F5740; }
.es-callout-success .es-callout-icon { background: var(--color-success); color: var(--color-base-elevated); }
/*
 * ESM-5 — the design's callout is ONE flowing block with a bold lead
 * (design/components.css: `.es-callout-body strong { font-weight: 600 }` and
 * nothing else). The theme had invented `display:block` on the title plus a
 * separate `<p class="es-callout-text">`, which split it into two stacked
 * blocks and made the live callout 81.5px against the mock's 62px. Rendering
 * the lead inline is what the design specifies; the `<p>` is gone from
 * patterns/activation-timing-callout.php for the same reason.
 */
.es-callout-title { font-weight: 600; }
.es-callout-text { margin: 0; font-size: 14.5px; line-height: 1.5; }
.es-callout-link { color: inherit; font-weight: 600; text-underline-offset: 3px; }

/* ─── Plan card (the buy surface) ─────────────────────────────────────────
 * src: japan.html .es-plan rect 281.5x237.8 — flex column, padding 22, gap 16,
 *      1px border, radius lg. Children: .pd-plan-tag 16.5 → .es-plan-head 50.3
 *      → .es-plan-meta 22.5 → .es-plan-foot 50.5 (margin-top 4).
 *      .es-plan-data / .es-plan-price-amount 24px/line-height 1;
 *      .es-plan-foot .es-btn-cta 235.5x50.5 (fills the card's content box).
 *      .pd-plans-grid: 4 cols gap 14 of the 1168 rail → 281.5.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-section-plans .wc-block-product-template,
.es-plan-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 14px;
  list-style: none;
  margin: 0;
  padding: 0;
}
.es-section-plans .wc-block-product-template > li { margin: 0; }

.es-plan-card {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 22px;
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-lg);
  position: relative;
  transition: border-color var(--transition-duration), box-shadow var(--transition-duration), transform var(--transition-duration);
}
.es-plan-card:hover { border-color: var(--color-neutral-strong); box-shadow: var(--shadow-md); transform: translateY(-3px); }
.es-plan-card--lifetime { border-color: var(--color-tier-lifetime); }

.es-pc-tier {
  font-size: 11px;
  line-height: 1.5;
  font-family: var(--font-family-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-tier-light);
}
.es-pc-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
.es-pc-data { min-width: 0; }
.es-pc-data-amount { font-family: var(--font-family-heading); font-size: 24px; font-weight: 700; letter-spacing: -0.025em; line-height: 1; }
.es-pc-validity { font-size: 13.5px; color: var(--color-contrast-muted); margin-top: 6px; line-height: 1.5; }
.es-pc-price { text-align: right; flex: 0 0 auto; }
.es-pc-price-val { font-family: var(--font-family-heading); font-size: 24px; font-weight: 700; letter-spacing: -0.025em; line-height: 1; }
.es-pc-per-gb { font-family: var(--font-family-mono); font-size: 11.5px; color: var(--color-contrast-muted); margin-top: 4px; line-height: 1.5; }
.es-pc-meta { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.es-pc-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border-radius: var(--radius-sm);
  font-size: 11px;
  line-height: 1.5;
  font-weight: 600;
  font-family: var(--font-family-mono);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.es-pc-badge-lifetime { background: rgba(184, 134, 43, 0.14); color: #8C641F; }
/* ESM-7: the always-present chip on a plan card. It replaced "Hotspot UNKNOWN"
   — see the comment in inc/helpers-product-card.php — and keeps `.es-pc-meta`
   occupied so the card's ported height (237.8) is unaffected by the removal. */
.es-pc-badge-data { background: var(--color-info-soft); color: var(--color-info); }
/* ESM-78 — "Get this plan" sits on ONE line across a row of cards.
 *
 * The cards were already equal height: the grid stretches them, and every card
 * in a row measured 254.3 on /esim-country/ni/ @1440. What moved was the CTA
 * inside them. Measured 2026-08-06, row 1: three cards put `.es-pc-actions` at
 * y=1176.6 and the fourth — "Global Multi-Country · 150 countries" — at
 * y=1193.1. The delta is 16.5px, which is exactly one line of the 11px/1.5
 * `.es-pc-tier` label. So it is the TIER LINE WRAPPING, not the badge count:
 * cards with one badge and cards with two both sat at 1176.6.
 *
 * The free space is pushed into the meta row's bottom margin rather than onto
 * `.es-pc-actions` as `margin-top: auto`, and that choice is load-bearing for
 * the gate: with `auto` on the actions, a card with NO slack would lose the
 * design's 4px offset (`.es-plan-foot` margin-top 4 in japan.html) and every
 * card would shorten by 4px, moving `plan-card-h` off its ported 237.8. This
 * way the no-slack case renders byte-identically and only the slack case moves.
 * Below 1024 the grid drops to two columns and then one, where each row is
 * shorter — the rule is inert there, which is correct, not a gap. */
.es-pc-meta { margin-bottom: auto; }
.es-pc-actions { display: flex; gap: 10px; align-items: center; margin-top: 4px; }
.es-pc-add-to-cart { flex: 1; }

/* Hotspot badge — the theme's name for the mock's `.es-hotspot`, whose unscoped
   duplicate in components.css was deleted in ESM-70. The three modifiers below
   are NOT dead: they are unreachable only because `_hotspot_supported` is
   `unknown` on every product Keepgo publishes. `esim_hotspot_modifier_class()`
   builds them and patterns/hotspot-badge.php renders them the day that changes. */
.es-hotspot-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 8px;
  border-radius: var(--radius-sm);
  font-size: 11px;
  line-height: 1.5;
  font-weight: 600;
  font-family: var(--font-family-mono);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  background: #EFEDE7;
  color: var(--color-contrast-muted);
}
.es-hotspot-badge .es-badge-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex: 0 0 auto; }
.es-hotspot-badge.es-hotspot--yes    { background: var(--color-success-soft); color: var(--color-hotspot-yes); }
.es-hotspot-badge.es-hotspot--no     { background: var(--color-error-soft);   color: var(--color-hotspot-no); }
.es-hotspot-badge.es-hotspot--capped { background: var(--color-warning-soft); color: var(--color-hotspot-capped); }

@media (max-width: 1023.98px) {
  .es-section-plans .wc-block-product-template, .es-plan-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 767.98px) {
  .es-section-plans .wc-block-product-template, .es-plan-grid { grid-template-columns: 1fr; }
}

/* ─── PDP details grid: networks + device compatibility ───────────────────
 * src: japan.html .pd-details-grid 1fr/1fr gap 24 on the 1168 rail → 572 each;
 *      .pd-net-card padding 28, radius lg, h3 20px/line-height 1.15 mb 16;
 *      .pd-net-row padding 12px 0 with a top divider (first row none).
 * NOTE: this replaces the previous mapping of `.es-coverage-list` onto
 * `.pd-coverage` — `.pd-coverage` is the coverage-MAP illustration in the hero
 * (505.5x481.4, padding 32), not the carrier list. The carrier list's design
 * counterpart is `.pd-net-card`, which is also what the heading compare pairs
 * ("Underlying networks", 20px).
 * ─────────────────────────────────────────────────────────────────────────── */
.es-pdp-details-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; align-items: start; }

.es-coverage-list {
  padding: 28px;
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-lg);
}
.es-coverage-title { font-size: 20px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.15; margin: 0 0 16px; }
.es-carriers { list-style: none; margin: 0; padding: 0; }
.es-carrier-item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 0;
  border-top: 1px solid var(--color-neutral);
  font-size: 15px;
  font-weight: 600;
}
.es-carrier-item:first-child { border-top: 0; }
.es-carrier-dot { color: var(--color-accent-1); font-size: 10px; line-height: 1; }
.es-coverage-note { font-size: 12.5px; color: var(--color-contrast-muted); margin: 16px 0 0; line-height: 1.5; }
.es-coverage-empty .es-coverage-note { margin: 0; }

@media (max-width: 1023.98px) { .es-pdp-details-grid { grid-template-columns: 1fr; } }

/* ─── Install stepper ─────────────────────────────────────────────────────
 * src: japan.html .pd-install dark card radius xl padding 56;
 *      .pd-install-stepchip 79.8x34 (padding 5px 12px, radius full);
 *      .pd-install-step 495.5x51 (padding 12px 0, gap 14, top divider);
 *      .pd-install-step-n 26x26 circle, mono 12px/700.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-install-stepper {
  background: var(--color-contrast);
  color: var(--color-base);
  border-radius: var(--radius-xl);
  padding: 56px;
}
.es-device-tabs { display: flex; gap: 6px; margin-bottom: 18px; flex-wrap: wrap; }
.es-device-tab {
  font: inherit;
  font-size: 12px;
  line-height: 24px;
  padding: 5px 12px;
  border: 0;
  border-radius: var(--radius-full);
  background: rgba(251, 247, 240, 0.08);
  color: rgba(251, 247, 240, 0.7);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.es-device-tab:hover { background: rgba(251, 247, 240, 0.14); color: var(--color-base); }
.es-device-tab.is-active,
.es-device-tab[aria-selected="true"] { background: var(--color-accent-1); color: #FFFFFF; }
/* ESM-70: `.es-device-tab-icon` deleted — patterns/install-stepper.php renders
   the device tabs as plain text labels, with no icon span. */

.es-steps-panel[hidden] { display: none; }   /* author rule beats UA-origin [hidden] (TBE-21) */
.es-steps-list { list-style: none; margin: 0; padding: 0; }
.es-step { display: flex; gap: 14px; padding: 12px 0; border-top: 1px solid rgba(251, 247, 240, 0.10); }
.es-step:first-child { border-top: 0; }
.es-step-num {
  width: 26px;
  height: 26px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: rgba(251, 247, 240, 0.10);
  display: grid;
  place-items: center;
  font-family: var(--font-family-mono);
  font-size: 12px;
  font-weight: 700;
  line-height: 1;
}
.es-step-body { font-size: 14px; line-height: 1.5; color: rgba(251, 247, 240, 0.85); min-width: 0; }
.es-step-title { font-size: 14px; font-weight: 600; margin: 0; line-height: 1.5; color: var(--color-base); }
.es-step-desc { margin: 2px 0 0; font-size: 14px; line-height: 1.5; }
.es-steps-compat-note {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 18px 0 0;
  padding-top: 16px;
  border-top: 1px solid rgba(251, 247, 240, 0.10);
  font-family: var(--font-family-mono);
  font-size: 12px;
  color: rgba(251, 247, 240, 0.55);
}

@media (max-width: 767.98px) { .es-install-stepper { padding: 28px; } }

/* ─── Single product (WC-native PDP) ──────────────────────────────────────
 * No dedicated mock — the country PDP is this site's primary product surface.
 * These rules exist so the WC add-to-cart form is a first-class control rather
 * than the browser default.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-pdp-wrap { display: flex; flex-direction: column; gap: 20px; align-items: flex-start; }
.es-pdp-meta { font-family: var(--font-family-mono); font-size: 12px; color: var(--color-contrast-muted); }
.es-pdp-price .woocommerce-Price-amount { font-family: var(--font-family-heading); }
.es-pdp-atc form.cart { display: flex; align-items: center; gap: 12px; margin: 0; }
.es-pdp-atc .quantity input {
  width: 84px;
  min-height: 50px;
  padding: 12px 14px;
  font: inherit;
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-md);
  background: var(--color-base-elevated);
}
.es-pdp-atc button.single_add_to_cart_button {
  background: var(--color-accent-1);
  color: #FFFFFF;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  padding: 14px 24px;
  min-height: 50.5px;
  border: 0;
  border-radius: var(--radius-full);
  cursor: pointer;
}
.es-pdp-atc button.single_add_to_cart_button:hover { background: var(--color-accent-1-hover); }

/* ESM-60 — the tethering disclosure (patterns/hotspot-badge.php, `unknown`
 * branch). Deliberately NOT a `.es-callout`: the callout family is reserved on
 * this page for the activation-timing warning, which is Critical-severity copy,
 * and giving an "we don't know" note the same weight would flatten the two.
 * A quiet, bordered note on the muted rail — read after the price, before the
 * add-to-cart. Colour is --color-contrast-muted (#5A6478 on #FBF7F0 = 5.57:1,
 * measured in ESM-49), the bold lead is full contrast. */
.es-hotspot-note {
  max-width: 44rem;
  margin: 0;
  padding: 12px 16px;
  border-left: 3px solid var(--color-neutral-strong);
  background: var(--color-base-elevated);
  border-radius: var(--radius-sm);
  font-size: 14px;
  line-height: 1.55;
  color: var(--color-contrast-muted);
}
.es-hotspot-note strong { display: block; color: var(--color-contrast); font-weight: 600; margin-bottom: 2px; }

/* ─── Cart & checkout (WooCommerce Blocks) ────────────────────────────────
 * src: cart.html    .ct-items 665 + .ct-summary 475 @x=829 → 28px gutter,
 *                   summary padding 28; .ct-item 665x146.3 padding 20;
 *                   .ct-checkout-btn 417x50.5.
 *      checkout.html .co-form 665 + .co-summary 475, same 28px gutter;
 *                   .co-input 607x48.5 (padding 12px 14px);
 *                   .co-place-btn 607x56 in the LEFT form column.
 * The WC blocks arrive from the pages' post_content carrying `alignwide`; the
 * design puts them on the 1168 content rail, so pin the width here rather than
 * rewriting DB content.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-cart-page .wp-block-woocommerce-cart,
.es-checkout-page .wp-block-woocommerce-checkout {
  max-width: 100%;
  margin-left: 0;
  margin-right: 0;
}
.es-cart-page .wc-block-cart,
.es-checkout-page .wc-block-checkout { max-width: none; }

.wc-block-cart .wc-block-cart__main,
.wc-block-checkout .wc-block-checkout__main { flex: 0 0 665px; max-width: 665px; min-width: 0; }
.wc-block-cart .wc-block-cart__sidebar,
.wc-block-checkout .wc-block-checkout__sidebar { flex: 0 0 475px; max-width: 475px; min-width: 0; }
/* Scope by the element ITSELF, never as a descendant: `.wc-block-cart` /
 * `.wc-block-checkout` sit ON the sidebar-layout node, and on checkout they ALSO
 * sit on an ancestor — so a descendant selector applied the gap on checkout and
 * silently skipped it on cart (measured: cart gap 0, checkout gap 28). */
.es-cart-page .wc-block-components-sidebar-layout,
.es-checkout-page .wc-block-components-sidebar-layout { gap: 28px; align-items: flex-start; }

.wc-block-components-sidebar .wc-block-components-order-meta,
.wc-block-cart__sidebar .wc-block-cart__totals-title { letter-spacing: 0.04em; }
.wc-block-cart__sidebar.wc-block-components-sidebar,
.wc-block-checkout__sidebar.wc-block-components-sidebar {
  padding: 28px;
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-lg);
}
.wc-block-cart-items__row { padding: 20px 0; }
.wc-block-cart__submit-button,
.wc-block-components-checkout-place-order-button {
  background: var(--color-accent-1);
  color: #FFFFFF;
  border-radius: var(--radius-full);
  font-size: 15px;
  font-weight: 600;
  min-height: 50.5px;
}
.wc-block-cart__submit-button:hover,
.wc-block-components-checkout-place-order-button:hover { background: var(--color-accent-1-hover); }
.wc-block-components-text-input input {
  min-height: 48.5px;
  padding: 12px 14px;
  border-radius: var(--radius-md);
  border: 1px solid var(--color-neutral);
  background: var(--color-base-elevated);
}
/* ESM-68 — this rule used to read `padding: 16px; border-radius: …`, and that
 * SHORTHAND was the whole defect.
 *
 * WooCommerce ships `padding: 16px 16px 16px 48px` on this option (identically
 * in both `packages-style.css` and `checkout.css`). The 48px is not decoration:
 * the radio input is taken out of flow — `position: absolute; left: 16px;
 * width: 20px` — so the left padding is the ONLY thing reserving room for it.
 *
 * Our shorthand reset all four longhands at equal specificity (0,1,0) from a
 * later stylesheet, so it won and set padding-left to 16px. The 20px input then
 * sat exactly on the first 20px of its own label. Measured on the deployed
 * checkout, all three widths: radio 152..172 / label.x 152 @1440, 48..68 / 48
 * @768, 36..56 / 36 @375 — a 20px overlap every time, i.e. "Test Payment
 * (simulated)" rendered as "…t Payment (simulated)". On the last screen before
 * payment, with the site's only payment method as the text being covered.
 *
 * The three sides we actually wanted are ALREADY 16px in WooCommerce's own
 * declaration, so the padding here was a no-op on three sides and destructive
 * on the fourth. It is therefore dropped entirely rather than restated as
 * longhands: the component owns the geometry that its own absolutely-positioned
 * input depends on, and the theme owns only the radius. Nothing here may
 * re-declare `padding` as a shorthand.
 *
 * Reference for the intended result: the terms checkbox on the same page uses
 * the same idiom and was always correct — 20px input, 12px clear gap to its
 * label (48 = 16 inset + 20 input + 12 gap). After this change the payment row
 * measures that same 12px gap.
 */
.wc-block-components-radio-control__option { border-radius: var(--radius-md); }

@media (max-width: 1023.98px) {
  .wc-block-cart .wc-block-cart__main,
  .wc-block-checkout .wc-block-checkout__main,
  .wc-block-cart .wc-block-cart__sidebar,
  .wc-block-checkout .wc-block-checkout__sidebar { flex: 1 1 100%; max-width: 100%; }
}

/* ─── Footer ──────────────────────────────────────────────────────────────
 * src: index.html .es-foot 1440x406.9 on a WHITE ground with a neutral top
 *      border; .es-foot-top padding 56 / max-width 1280; .es-foot-grid 1168
 *      (1.4fr + 4x1fr, gap 48); .es-foot-bottom padding 18px 56px, 13px text.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-site-footer { border-top: 1px solid var(--color-neutral); }
/* The footer group is `layout: constrained`, so core emits
 * `.wp-container-N > * + * { margin-block-start: var(--wp--style--block-gap) }`
 * — 24px of blockGap between the footer body and the bottom bar that the design
 * does not have (measured: body ends 3602.4, bottom bar starts 3626.5, i.e. the
 * footer stood 24px taller than its own parts add up to). The two children own
 * their spacing via their own padding; the gap between them is zero.
 * Selector is `footer.es-site-footer` (0,1,1) so it outranks core's
 * `.wp-container-N` layout rule (0,1,0) regardless of stylesheet order. */
footer.es-site-footer > * + * { margin-block-start: 0; }

/* ESM-6/ESM-4: the `.es-footer-trust` strip (Instant delivery / Visa · Mastercard
   · Stripe / 193+ countries) is GONE from parts/footer.html — it was not in the
   design and two of its four rows were unearned claims on every page. The rule
   is kept as a no-op guard so a stray strip re-inserted via the Site Editor does
   not land unstyled; the markup that used it no longer exists.
   src: index.html .es-foot 1440x406.9 on a white ground; .es-foot-top padding 56;
        .es-foot-grid 1168 (1.4fr + 4x1fr, gap 48); .es-foot-bottom padding 18px.
   Live target is 358.4, i.e. the mock minus the payment badges (no card gateway
   — see patterns/footer-bottom.php) and minus the currency/language dead
   controls. Derivation recorded in configs/esim-store.mjs `footer-h`. */
.es-footer-body {
  padding: 56px 0;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 48px;
  flex-wrap: wrap;
}
.es-footer-brand { max-width: 300px; }
.es-footer-tagline { font-size: 14.5px; margin: 16px 0 0; max-width: 260px; line-height: 1.55; }
/* ESM-44 — the tagline used to carry `textColor: contrast-muted`, i.e. #5A6478,
 * the token for muted text on the CREAM ground. On the footer's #0F1A2E it
 * measured 2.92:1 against WCAG AA's 4.5:1, on every page of the site.
 * `--wp--custom--on-contrast--muted` is the footer's own established idiom (the
 * column titles, copyright and supplier line already used this exact value,
 * hardcoded twice); it measures 5.69:1 and still reads as secondary against the
 * full-strength #FBF7F0 links at 16.28:1. Declared once in theme.json
 * settings.custom so the value is a token, not three literals. */
.es-site-footer .es-footer-tagline { color: var(--wp--custom--on-contrast--muted); }
.es-footer-nav-cols { display: flex; gap: 48px; flex-wrap: wrap; align-items: flex-start; }
.es-footer-col { min-width: 140px; }
.es-footer-col-title {
  font-family: var(--font-family-heading);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  margin: 0 0 14px;
  color: var(--color-contrast-muted);
}
.es-site-footer .es-footer-col-title { color: var(--wp--custom--on-contrast--muted); }
.es-footer-links { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.es-footer-links li { margin: 0; }
.es-footer-links a { font-size: 14.5px; text-decoration: none; color: inherit; }
.es-footer-links a:hover { text-decoration: underline; text-underline-offset: 3px; }
.es-footer-bottom {
  padding: 18px 0;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  font-size: 13px;
}
.es-footer-bottom p { margin: 0; }
.es-footer-copyright,
.es-footer-supplier { color: var(--color-contrast-muted); }
.es-site-footer .es-footer-copyright,
.es-site-footer .es-footer-supplier { color: var(--wp--custom--on-contrast--muted); }

@media (max-width: 767.98px) {
  .es-footer-body { flex-direction: column; gap: 32px; }
  .es-footer-nav-cols { gap: 32px; }
}

/* ─── Account / My eSIMs ──────────────────────────────────────────────────
 * src: my-esims.html .me-side 240 wide (padding 22) + .me-stats 896 @x=408 →
 *      32px gutter (NOT the 28 used by cart/checkout/catalog — the account
 *      layout has its own gutter in the design);
 *      .me-side-link 194x41 (padding 10px 12px); .me-row 894x40.5;
 *      .es-status 74.7x26 (padding 4px 10px, radius full).
 * ─────────────────────────────────────────────────────────────────────────── */
/* ESM-20 — SELECTOR CORRECTION. This rule used to target
 * `.es-account-page .woocommerce-account`. `woocommerce-account` is a BODY
 * class (WooCommerce adds it via `wc_body_class()`), not a wrapper class, so
 * the descendant selector matched nothing and the two-column account layout
 * never applied — invisible for as long as the account area rendered no content
 * at all. The element `[woocommerce_my_account]` actually emits is
 * `div.woocommerce`, nested inside the `wp:post-content` wrapper.
 *
 * `max-width: none` undoes WooCommerce's own block-theme stylesheet, which pins
 * `.woocommerce` to 1000px and centres it — that pushed the account columns off
 * the site's 1168 content rail (measured x=220 against the h1's x=136).
 *
 * src: my-esims.html .me-side 240 (pad 22) + .me-stats 896 @x=408 → 32px gutter
 *      (NOT the 28 used by cart/checkout/catalog — the account layout has its
 *      own gutter in the design). 240 + 32 + 896 = 1168 = the content rail.
 */
/* ESM-72 — `body.logged-in` is LOAD-BEARING, and its absence was the whole of
 * the signed-out account defect.
 *
 * This grid describes the SIGNED-IN account page: a 240px navigation column
 * beside the content. Signed OUT, WooCommerce renders one child inside the same
 * `.woocommerce` element — `#customer_login`, or the password forms — and that
 * single child was placed in the 240px NAV TRACK. Measured on the deployed site
 * 2026-08-06 @1440: `#customer_login` 240 wide inside a 1168 rail, its two
 * columns 115.2 each (WooCommerce's own 48% of 240, arriving correctly and
 * being applied to the wrong box), the "Username or email address" label
 * 67.2 wide — one word per line — and the reset form's two password fields
 * 106.8 each. Below 1024 the media query collapses this to `1fr`, which is why
 * the same page looks merely plain at 768/375 and broken at 1440.
 *
 * So the columns did not "collapse for want of CSS": they were sized by a rule
 * written for a different page state. Scoping it to the state it describes is
 * the fix; the signed-out layout is built below rather than inherited. */
body.logged-in .es-account-page .woocommerce,
body.logged-in .es-my-esims-page .woocommerce,
body.logged-in .es-orders-page .woocommerce {
  display: grid;
  grid-template-columns: 240px 1fr;
  gap: 32px;
  align-items: start;
}
/* Specificity, not preference. Three WooCommerce stylesheets fight for this
 * layout and each of them outranks a plain `.es-account-page .woocommerce`:
 *   woocommerce-blocktheme.css  `.woocommerce-account main .woocommerce`  (0,3,1)
 *       → max-width: 1000px, which parks the account columns off the site's
 *         1168 content rail (measured: h1 at x=136, columns at x=220).
 *   woocommerce-layout.css      `.woocommerce-account .woocommerce-MyAccount-navigation`
 *                               / `… .woocommerce-MyAccount-content`        (0,2,0)
 *       → width 30% / 68% + float left/right, i.e. WooCommerce's classic
 *         float layout applied on top of our grid. It sized both columns as
 *         percentages of the wrong track and put the nav in column 2.
 * The selectors below are deliberately one step heavier than each of those and
 * place both children EXPLICITLY, so auto-placement is not in play at all. */
body.woocommerce-account main.es-account-page .woocommerce { max-width: none; width: 100%; }

/* ESM-101 — the SIGNED-OUT account page is a centred measure, not a rail.
 *
 * `.es-measure-grid` is written on the element in templates/page-my-account.html
 * (one construction, shared with the legal documents and /about/ — read the
 * comment in components.css). All this does is choose the track width and hand
 * the page back to flow layout for the signed-IN state, where the account area
 * is a genuine 1168 two-column layout and a 46rem track would be wrong.
 *
 * Scoped on `body.logged-in`, the same class ESM-72 established as load-bearing
 * here: every endpoint of this page renders through the same template, so the
 * state — not the URL — is what decides which layout applies. */
main.es-account-page { --es-measure: var(--measure-auth); }
body.woocommerce-lost-password main.es-account-page { --es-measure: var(--measure-auth-narrow); }
body.logged-in main.es-account-page.es-measure-grid { display: block; }
main.es-account-page .woocommerce > .woocommerce-MyAccount-navigation {
  width: auto;
  float: none;
  grid-column: 1;
  grid-row: 1;
}
main.es-account-page .woocommerce > .woocommerce-MyAccount-content {
  width: auto;
  float: none;
  grid-column: 2;
  grid-row: 1;
}
.woocommerce-MyAccount-navigation {
  padding: 22px;
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-lg);
}
.woocommerce-MyAccount-navigation ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
/* `woocommerce-blocktheme.css` sets `padding: 1em 0` on these <li>s with
 * selector `.woocommerce-account .woocommerce-MyAccount-navigation li` (0,2,1),
 * which inflated each row to 57.8px while the <a> inside stayed at the design's
 * 41.8. Exactly the SKF-56 shape the config comment on `account-navrow-li-h`
 * predicted — which is why the gate asserts BOTH the <a> and the <li>: the <a>
 * stays green right through this failure.
 * `main.es-account-page …` is (0,2,2), one step above WC's rule. A plain
 * `.woocommerce-MyAccount-navigation li` (0,1,1) loses and was a no-op.
 * src: my-esims.html .me-side-link 194x41 (pad 10px 12px). */
main.es-account-page .woocommerce-MyAccount-navigation li { margin: 0; padding: 0; border: 0; list-style: none; }
main.es-account-page .woocommerce-MyAccount-navigation li::before,
main.es-account-page .woocommerce-MyAccount-navigation li::after { content: none; }
.woocommerce-MyAccount-navigation li a {
  display: flex;
  align-items: center;
  padding: 10px 12px;
  min-height: 41px;
  border-radius: var(--radius-md);
  font-size: 14.5px;
  text-decoration: none;
  color: var(--color-contrast);
}
.woocommerce-MyAccount-navigation li a:hover { background: var(--color-base); }
.woocommerce-MyAccount-navigation li.is-active a { background: var(--color-contrast); color: var(--color-base); font-weight: 600; }
.woocommerce-MyAccount-content { min-width: 0; }

.es-my-esims { min-width: 0; }
.es-my-esims-table {
  width: 100%; border-collapse: collapse; font-size: 14px;
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.es-my-esims-table th,
.es-my-esims-table td { padding: 12px 20px; text-align: left; border-bottom: 1px solid var(--color-neutral); }
.es-my-esims-table tbody tr:last-child td { border-bottom: 0; }

/* ESM-26 — the header row measured 66.5px against the design's 40.5px.
 * Cause: the head inherited the table's 14px/21px body type, so "Country / Plan"
 * took two lines inside a 129px column → 12 + 2×21 + 12 = 66.5. The design's
 * `.me-row--head` is an 11px uppercase micro-label (`my-esims.html`, measured
 * 894x40.5 @1440: padding 12/12 + one 16.5px line). Height was already declared
 * as 40.5 on the `tr` — but `height` on a table row is a MINIMUM, so it could
 * never pull an over-tall row back down; the type scale is the actual contract.
 * `nowrap` states the intent explicitly instead of relying on 11px happening to
 * fit: a longer column label must widen its column, never re-wrap the row. */
.es-my-esims-table thead th {
  font-size: 11px;
  line-height: 1.5;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-weight: 600;
  color: var(--color-contrast-faint);
  white-space: nowrap;
  background: var(--color-base);
}
/* src: my-esims.html `.me-iccid` — 12px mono, muted, INLINE. It used to inherit
 * the boxed `.es-lpa-string, .es-lpa-code, .es-mono-code, .es-iccid` rule
 * (12px padding + border + background), which turned a table cell into a nested
 * card 63.5px tall and drove the body row to 100px. */
.es-iccid { font-family: var(--font-family-mono); font-size: 12px; color: var(--color-contrast-muted); background: none; border: 0; padding: 0; }
.es-my-esims-table .es-actions { white-space: nowrap; }
.es-my-esims-table .es-actions .es-btn { margin-right: 6px; }
.es-my-esims-table .es-actions .es-btn:last-child { margin-right: 0; }
.es-my-esims-table .es-btn.is-active { background: var(--color-accent-1); color: #FFFFFF; border-color: var(--color-accent-1); }

/* ESM-27 — the row's expandable QR panel. `hidden` on a <tr> only works while
 * nothing sets `display` on it, so this rule deliberately styles the CELL. */
.es-esim-detail > td { background: var(--color-base); padding: 20px; }
.es-esim-detail-inner { display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap; }
.es-esim-detail-inner .es-qr-image-wrap { width: 320px; max-width: 100%; }
.es-esim-detail-note { font-size: 13px; color: var(--color-contrast-muted); max-width: 40ch; margin: 0; line-height: 1.55; }
.es-esim-install-reveal { margin-top: 24px; }
.es-status-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border-radius: var(--radius-full);
  font-size: 12px;
  line-height: 18px;
  font-weight: 600;
  font-family: var(--font-family-mono);
  background: #EFEDE7;
  color: var(--color-contrast-muted);
}
.es-status-pill.is-active    { background: var(--color-success-soft); color: var(--color-status-active); }
.es-status-pill.is-installed { background: var(--color-info-soft);    color: var(--color-status-installed); }
.es-status-pill.is-expiring  { background: var(--color-warning-soft); color: var(--color-status-expiring); }
.es-status-pill.is-depleted  { background: var(--color-error-soft);   color: var(--color-status-depleted); }
.es-row-sub { font-size: 12.5px; color: var(--color-contrast-muted); }

/* ═══ SIGNED-OUT ACCOUNT SCREENS (ESM-72 / ESM-73) ═════════════════════════
 *
 * /my-account/ for a visitor who has not signed in, and the two password
 * screens behind it (`/my-account/lost-password/`, with and without a `?key=`).
 * Before this block the theme had NO rule naming any of WooCommerce's signed-out
 * form classes — `customer_login`, `u-columns`, `col2-set`, `woocommerce-form-*`
 * and `woocommerce-ResetPassword` matched nothing in any of the four
 * stylesheets. That is the front door of the site.
 *
 * Why nothing caught it, which matters more than the pixels: every check this
 * site has ever run against the account area — fidelity, commerce, the eleven
 * my-esims assertions — runs with `auth: 'shopper'`, because run anonymously
 * they would all report "selector not found" and pass vacuously (the ESM-20
 * shape). So the ONE state in which this page is broken is the one state no
 * gate has ever looked at. The anonymous `my-account` / `my-account-login` /
 * `my-account-lost-password` entries added to configs/esim-store.mjs alongside
 * this fix are the actual repair; the CSS is what they now hold in place.
 *
 * Vocabulary is the site's own, not a second system: the card is the same
 * elevated/neutral/radius-lg card as `.woocommerce-MyAccount-navigation` and
 * `.ct-summary`, the field is the same box as
 * `.wc-block-components-text-input input` on the checkout, and the submit is the
 * page's `.es-btn-cta`. No new token is introduced.
 *
 * SPECIFICITY NOTES — three WooCommerce sheets own this markup:
 *   `.woocommerce .col2-set .col-1`          (0,3,0)  float:left; width:48%
 *   `.woocommerce form.login`                (0,2,1)  border + padding + radius
 *   `.woocommerce form .form-row`            (0,3,1)  padding:3px; margin:0 0 6px
 * `#customer_login` carries an ID, so the column rules below win outright; the
 * form and row rules restate WooCommerce's own scope with `.es-account-page` in
 * front. Nothing here relies on a specificity TIE being broken by load order.
 * ═════════════════════════════════════════════════════════════════════════ */

/* ── The Login / Register pair ─────────────────────────────────────────── */
/* The auth area is a column, not a page-wide band: `--measure-auth` (46rem) =
 * two 22rem cards plus the 24px gutter between them.
 *
 * ESM-101 — IT IS NOW CENTRED, AND SO IS THE h1 ABOVE IT. The ESM-72 note this
 * replaces argued for starting the column on the left rail, because "a centred
 * form beneath a left-aligned title reads as two unrelated pages". The reading
 * was right; the conclusion was not. What it produced, measured on the deployed
 * 0.16.0 at 1440, was `#customer_login` 736 wide at x=136 in a rail of
 * x=136…1304 — a 432px dead column down the right of the only page a signed-out
 * visitor is sent to, and the third of three cards the test team filed on the
 * same afternoon about the same shape.
 *
 * The title's objection is answered by moving the title, not by leaving the
 * form: `main.es-account-page` becomes the measure grid (components.css), so
 * the h1 and the card pair share ONE centred 46rem track and one left edge.
 * Nothing is centre-ALIGNED — the text still reads from a common left edge.
 *
 * The width is no longer capped on `#customer_login` itself. The grid track
 * carries it, which also removes the trap the old comment documented: the
 * `.woocommerce` wrapper is a direct child of
 * `.entry-content.is-layout-constrained`, so WordPress applies
 * `margin-left/right: auto !important` to it and no max-width there could be
 * pulled back off centre. A grid track is not a margin and does not enter that
 * fight at all. */
#customer_login {
  display: grid;
  grid-template-columns: 1fr;
  gap: 20px;
  align-items: start;
}
/* WooCommerce ships a float CLEARFIX on `.col2-set`
 * (`::before` / `::after` with `content: " "; display: table`). In a float
 * layout those are invisible; in a GRID they are two extra grid ITEMS, and they
 * took the first and last cells. Measured after the first deploy @1440: the
 * Login column landed in grid column 2 at x=732 and Register wrapped to row 2
 * at x=352 — the two forms diagonally opposite each other. `content: none`
 * removes the boxes outright; `display: none` would leave the pseudo-elements
 * generated and is the weaker statement. The ID gives this (1,0,0) against
 * WooCommerce's (0,3,0), so it wins outright. */
#customer_login::before,
#customer_login::after { content: none; }
#customer_login > .col-1,
#customer_login > .col-2 {
  float: none;
  width: auto;
  margin: 0;
  padding: 28px;
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-lg);
}
/* WooCommerce's own card (1px #ccc, 20px padding, 5px radius) would draw a
 * second box inside this one — the nested-pill shape. The column IS the card. */
.es-account-page .woocommerce form.woocommerce-form-login,
.es-account-page .woocommerce form.woocommerce-form-register {
  border: 0;
  padding: 0;
  margin: 0;
  border-radius: 0;
}
/* "Login" / "Register" are the cards' headings, at the site's card-title size
 * (the same 20px/-0.02em as `.ct-summary h2` and the cart totals title). */
#customer_login > div > h2 {
  margin: 0 0 18px;
  font-family: var(--font-family-heading);
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.2;
}

/* ── Fields ────────────────────────────────────────────────────────────── */
.es-account-page .woocommerce form .form-row {
  padding: 0;
  margin: 0 0 18px;
}
.es-account-page .woocommerce form .form-row:last-of-type { margin-bottom: 0; }
.es-account-page .woocommerce form .form-row label {
  display: block;
  margin: 0 0 6px;
  font-size: 13.5px;
  font-weight: 500;
  line-height: 1.4;
  color: var(--color-contrast-muted);
}
/* The required marker is the one place a colour is asserted here, and it has to
 * carry meaning without relying on hue alone — WooCommerce already emits a
 * `<span class="screen-reader-text">Required</span>` next to it, so the glyph is
 * decoration for sighted users and the label is complete without it. */
.es-account-page .woocommerce form .form-row label .required {
  color: var(--color-error);
  text-decoration: none;
  border: 0;
}
.es-account-page .woocommerce form .form-row .input-text,
.es-account-page .woocommerce form .form-row input[type="text"],
.es-account-page .woocommerce form .form-row input[type="email"],
.es-account-page .woocommerce form .form-row input[type="password"] {
  /* Same field box as the checkout's `.wc-block-components-text-input input`. */
  width: 100%;
  min-height: 48.5px;
  padding: 12px 14px;
  font: inherit;
  font-size: 15px;
  color: var(--color-contrast);
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-md);
  box-shadow: none;
}
.es-account-page .woocommerce form .form-row .input-text:focus,
.es-account-page .woocommerce form .form-row input:focus {
  outline: 2px solid var(--color-accent-1);
  outline-offset: 1px;
  border-color: var(--color-accent-1);
}
/* WooCommerce wraps password fields in `<span class="password-input">` and
 * absolutely positions a show/hide button inside it. The span defaults to
 * inline, so it did not establish the containing block the button expects. */
.es-account-page .woocommerce form .form-row .password-input {
  display: block;
  position: relative;
  width: 100%;
}
.es-account-page .woocommerce form .form-row .show-password-input {
  top: 50%;
  transform: translateY(-50%);
  right: 14px;
}

/* "Remember me" — a checkbox row, not a field row. */
.es-account-page .woocommerce form .woocommerce-form-login__rememberme {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 18px;
  font-size: 14px;
  color: var(--color-contrast);
}
.es-account-page .woocommerce form .woocommerce-form-login__rememberme input {
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: var(--color-accent-1);
}

/* ── Submits ───────────────────────────────────────────────────────────── */
/* Deliberately NOT restyled. WooCommerce puts `wp-element-button` on all three
 * of these buttons, so they already render as the site's CTA out of theme.json —
 * background, radius, padding and hover all come from
 * `styles.elements.button`. Restating any of that here would be a second copy of
 * a token set that theme.json owns, which is exactly the shape of the ESM-71
 * defect fixed in this same release. Only the row rhythm around them is set. */
.es-account-page .woocommerce form .form-row button[type="submit"] { margin: 0; }
.es-account-page .woocommerce .woocommerce-LostPassword {
  margin: 16px 0 0;
  font-size: 13.5px;
}
.es-account-page .woocommerce .woocommerce-LostPassword a { color: var(--color-accent-1-hover); }

/* ── Prose inside the forms ────────────────────────────────────────────── */
/* The register column's explanatory line and the privacy notice. Both used to
 * be measured at 73px wide (552px tall) inside the 240px track. */
/* `:not(.form-row)` is not decoration — WooCommerce renders its FIELD rows as
 * `<p class="woocommerce-form-row form-row …">`, so a bare `form > p` would
 * repaint every input row in muted 13.5px prose. */
.es-account-page .woocommerce form.woocommerce-form-register > p:not(.form-row),
.es-account-page .woocommerce .woocommerce-privacy-policy-text p,
.es-account-page .woocommerce .woocommerce-ResetPassword > p:not(.form-row) {
  margin: 0 0 18px;
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--color-contrast-muted);
}
.es-account-page .woocommerce .woocommerce-privacy-policy-text a { color: var(--color-accent-1-hover); }

/* ── Password strength meter ───────────────────────────────────────────── */
/* WooCommerce prints this INSIDE the field's row, directly under the input, and
 * ships it as a full-bleed coloured bar with no width relation to the field.
 * Pinned to the field's own box and given the site's status tokens. */
.es-account-page .woocommerce .woocommerce-password-strength {
  display: block;
  width: 100%;
  margin: 8px 0 0;
  padding: 6px 10px;
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.4;
  text-align: left;
  border-radius: var(--radius-sm);
  background: var(--color-neutral);
  color: var(--color-contrast-muted);
}
.es-account-page .woocommerce .woocommerce-password-strength.short,
.es-account-page .woocommerce .woocommerce-password-strength.bad {
  background: var(--color-error-soft);
  color: var(--color-error);
}
.es-account-page .woocommerce .woocommerce-password-strength.good {
  background: var(--color-warning-soft);
  color: var(--color-warning);
}
.es-account-page .woocommerce .woocommerce-password-strength.strong {
  background: var(--color-success-soft);
  color: var(--color-success);
}
.es-account-page .woocommerce .woocommerce-password-hint {
  display: block;
  margin: 6px 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--color-contrast-muted);
}

/* ── The password screens ──────────────────────────────────────────────── */
/* One card, like a login column. Used by BOTH the "email me a link" request
 * form and the "set a new password" form — WooCommerce gives them the same
 * class pair (`woocommerce-ResetPassword lost_reset_password`) and the same
 * body class, `woocommerce-lost-password`, which is what narrows the measure
 * track below.
 *
 * ESM-101 cross-surface: this screen carried the identical defect and was on
 * none of the three cards. Measured on the deployed 0.16.0 at 1440:
 * 422 wide at x=165, i.e. 853px of dead space to its right. `max-width: 30rem;
 * margin: 0` — the same two declarations, the same result. The width now comes
 * from the grid track, so there is nothing here left to pin. */
.es-account-page .woocommerce .woocommerce-ResetPassword {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 20px;
  padding: 28px;
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-lg);
}
.es-account-page .woocommerce .woocommerce-ResetPassword > * { grid-column: 1 / -1; }
/* WooCommerce floats these two at 47%. In the 240px track that produced two
 * 106.8px password fields side by side; at any width a float is the wrong tool
 * here, because the strength meter under field 1 changes its height and drags
 * the "Save" button with it. */
.es-account-page .woocommerce .woocommerce-ResetPassword .form-row-first,
.es-account-page .woocommerce .woocommerce-ResetPassword .form-row-last {
  float: none;
  width: auto;
}
.es-account-page .woocommerce .woocommerce-ResetPassword .clear { display: none; }

@media (min-width: 768px) {
  #customer_login { grid-template-columns: 1fr 1fr; gap: 24px; }
}
/* 480, not 768: the two password fields are short and the card is 30rem wide,
 * so they pair up well before the login columns do.
 *
 * `:has(.form-row-last)` is the guard that matters. WooCommerce reuses the SAME
 * two classes for the "email me a link" REQUEST form, which has one field —
 * carrying `form-row-first` and no partner. Without this the single "Username or
 * email" input would render at half the card's width for no reason. An engine
 * without `:has()` keeps both screens single-column, which is the safe way to
 * be wrong. */
@media (min-width: 480px) {
  .es-account-page .woocommerce .woocommerce-ResetPassword:has(.form-row-last) { grid-template-columns: 1fr 1fr; }
  .es-account-page .woocommerce .woocommerce-ResetPassword:has(.form-row-last) > .form-row-first { grid-column: 1; }
  .es-account-page .woocommerce .woocommerce-ResetPassword:has(.form-row-last) > .form-row-last  { grid-column: 2; }
}

@media (max-width: 1023.98px) {
  body.logged-in .es-account-page .woocommerce,
  body.logged-in .es-my-esims-page .woocommerce,
  body.logged-in .es-orders-page .woocommerce { grid-template-columns: 1fr; }
  main.es-account-page .woocommerce > .woocommerce-MyAccount-navigation { grid-column: 1; grid-row: 1; }
  main.es-account-page .woocommerce > .woocommerce-MyAccount-content    { grid-column: 1; grid-row: 2; }

  /* ESM-36 — below 1024 the eSIM table stops being a table.
   *
   * Seven columns need 832px and the table did not adapt, so the document
   * scrolled sideways to 852 at 375 and 864 at 768 — the only page on the site
   * that overflowed. "Show QR" sat at x=668…756, entirely off a 375 screen,
   * with no affordance that anything existed to the right. That is the control
   * that re-issues the activation code, on the screen a traveller opens from a
   * phone, abroad, to install their eSIM.
   *
   * One card per eSIM, label on the left, value on the right — so ICCID and
   * Status stay READ-ALOUD-able for a support call, which is why simply
   * shrinking the type was not an option. The `<thead>` is visually hidden
   * rather than removed: it is still the accessible column header for the
   * cells, and `data-label` (patterns/my-esims-list.php) repeats the same
   * strings visually.
   *
   * The threshold is 1024, not 768, because the account layout stacks here and
   * the table needs ~832 + gutters: at 768 the rail is 704 and at 1024 it is
   * 960, so 1024 is the last width where the table genuinely fits. */
  .es-my-esims-table {
    display: block;
    background: transparent;
    border: 0;
    border-radius: 0;
    overflow: visible;
  }
  /* `display: none`, not a visually-hidden clip. Once the <table> is
   * `display: block` the element loses its table role outright, so a surviving
   * <thead> is no longer the accessible header for anything — it is just seven
   * off-screen boxes (measured: four of them still reported rects past the
   * viewport edge). The column names are carried instead by `data-label`, whose
   * ::before content is announced by NVDA/JAWS/VoiceOver and, unlike the
   * <thead>, is also VISIBLE next to its own value. */
  .es-my-esims-table thead { display: none; }
  .es-my-esims-table tbody,
  .es-my-esims-table tr,
  .es-my-esims-table td { display: block; width: 100%; }

  /* MANDATORY next to the `display: block` above — TBE-21. `hidden` is a
   * USER-AGENT rule and author CSS beats the UA stylesheet by ORIGIN, decided
   * before specificity and before source order. Without this line the collapsed
   * QR panel would paint on every card while `el.hidden` still read true. */
  .es-my-esims-table tr[hidden],
  .es-my-esims-table td[hidden] { display: none; }

  .es-my-esims-table tbody tr:not(.es-esim-detail) {
    background: var(--color-base-elevated);
    border: 1px solid var(--color-neutral);
    border-radius: var(--radius-lg);
    margin-bottom: 12px;
    overflow: hidden;
  }
  .es-my-esims-table td {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 16px;
    padding: 10px 16px;
    text-align: right;
  }
  .es-my-esims-table td[data-label]::before {
    content: attr(data-label);
    flex: 0 0 auto;
    font-size: 11px;
    line-height: 1.5;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    font-weight: 600;
    color: var(--color-contrast-faint);
    text-align: left;
  }
  .es-my-esims-table td.es-cell-primary {
    display: block;
    text-align: left;
    background: var(--color-base);
    padding: 14px 16px;
  }
  .es-my-esims-table tbody tr:not(.es-esim-detail) td:last-child { border-bottom: 0; }

  /* Actions stop being a nowrap strip and become two full-width targets. 44px
   * is the WCAG 2.2 SC 2.5.8 floor and the token this theme already carries. */
  .es-my-esims-table .es-actions {
    display: flex;
    gap: 8px;
    white-space: normal;
    padding: 12px 16px;
  }
  .es-my-esims-table .es-actions .es-btn {
    flex: 1 1 0;
    justify-content: center;
    margin-right: 0;
    min-height: var(--touch-target);
  }

  /* `text-align: left` re-stated: the card rule above right-aligns every <td>
   * so a value sits opposite its label, and the QR panel is a <td> too — the
   * "scan this on the phone that will use the data" paragraph inherited it and
   * rendered ragged-left prose. */
  .es-esim-detail > td { padding: 16px; text-align: left; }
  /* Cap at the PNG's own 320px: `width: 100%` alone stretched a 320px QR to
   * 634px at 768, i.e. upscaled it, and a blurry QR is a QR that may not scan.
   * `max-width: 100%` still shrinks it on a 375 screen (measured 265). */
  .es-esim-detail-inner .es-qr-image-wrap { width: 100%; max-width: 320px; }
  .es-esim-detail-inner .es-qr-image { max-width: 100%; height: auto; }
}

/* ESM-40 / ESM-KAI-28 — the order NUMBER disappears on a phone.
 *
 * WooCommerce's own `woocommerce-smallscreen.css` de-tables `shop_table_responsive`
 * below 769px and, in doing so, does `table.shop_table_responsive tbody th
 * { display: none }` — while its label fallback,
 * `tr td::before { content: attr(data-title) ": " }`, is scoped to `td`.
 * The order number is the row's `<th scope="row">`, so it is the one cell that
 * both rules miss: measured at 375, `display: none`, rect 0x0, `::before` none,
 * with "#2125" inside it. The customer saw four unlabelled values —
 * "July 30, 2026 / Completed / $0.67 for 1 item / View" — and support had
 * nothing to ask them for.
 *
 * Restored as a normal labelled row, matching WooCommerce's own convention for
 * the sibling cells (bold label floated left, value right) so the card reads as
 * one list. Specificity is one class above WC's `.woocommerce table.…` rule,
 * not `!important` — the cascade is winnable here.
 * The breakpoint is WooCommerce's 768, not the theme's 1024, because the rule
 * being corrected is WooCommerce's and it must switch on the same edge. */
@media only screen and (max-width: 768px) {
  .es-orders-page .woocommerce table.shop_table_responsive tbody th {
    display: block;
    width: 100%;
    text-align: right;
    font-weight: 400;
  }
  .es-orders-page .woocommerce table.shop_table_responsive tbody th::before {
    content: attr(data-title) ": ";
    font-weight: 700;
    float: left;
  }
}

/* ─── Country PDP related destinations ────────────────────────────────────
 * `.pd-related-grid` (4 cols, 14px gap) is already in pages.css from the mock.
 * The theme's tile class is `.es-country-tile`, the mock's is `.es-tile`, so the
 * aspect-ratio rule has to be aliased or the tiles fall back to content height.
 * src: japan.html .pd-related-grid .es-tile { aspect-ratio: 5/4.5 }.
 * ─────────────────────────────────────────────────────────────────────────── */
.pd-related-grid .es-country-tile { aspect-ratio: 5 / 4.5; }
.es-related-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; margin-bottom: 24px; }
/* `.es-related-head .es-related-title` (0,2,0), not a bare class. TWO rules
 * outrank a single class here and both are (0,1,1):
 *   components.css   `.es-section h2`      → var(--font-size-xl) = 30px
 *   theme-bridge.css `.es-section-head h2` → 30px
 * The section sits inside `.es-section` (the template group), so the first one
 * applies no matter what classes the head carries.
 * src: japan.html `.pd-related-grid` head h2 = 26px. */
.es-related-head .es-related-title { margin-top: 8px; font-size: 26px; font-weight: 600; letter-spacing: -0.025em; }

/* ─── Country PDP FAQ (ESM-10) ────────────────────────────────────────────
 * The `.pd-faq` / `.pd-faq-list` geometry is already ported wholesale into
 * pages.css from design/japan.html (two columns 1.2fr/1fr, 56px gap; accordion
 * card with 56px summary rows, 18px/22px padding). Only the intro column needs
 * theme-side rules — the mock styles it with inline attributes.
 * ─────────────────────────────────────────────────────────────────────────── */
.es-pdp-faq-intro h2 { margin-top: 8px; }
.es-pdp-faq-lead { color: var(--color-contrast-muted); margin-top: 14px; font-size: 15px; line-height: 1.55; }
.es-pdp-faq-lead a { color: var(--color-accent-1-hover); }
.es-pdp-faq-list summary { gap: 16px; }
/* `<summary>` is focusable; the UA outline is suppressed by the reset, so the
   focus ring has to be restored explicitly (WCAG 2.1 AA, 2.4.7). */
.es-pdp-faq-list summary:focus-visible { outline: 2px solid var(--color-accent-1); outline-offset: -2px; }

/* ─── Order confirmation (QR delivery) — ESM-12 ────────────────────────────
 * Ported from order-confirmation.html, measured in Chromium @1440 (dsf=1,
 * --hide-scrollbars) 2026-07-30:
 *   .oc-hero          1440x201.9, padding 56px 0 28px  → .es-oc-hero
 *   .oc-tick          64x64                            → .es-oc-tick
 *   .oc-h1            534x48.4 @x=224 (fs 44)          → .es-oc-h1
 *   .oc-email         1168x74, padding 18px 24px       → .es-oc-note
 *   .oc-qr-block      1168x484.5, padding 32, radius xl→ .es-qr-card
 *   .oc-qr-grid       320px 1fr, gap 32                → .es-qr-grid
 *   .oc-qr-canvas-wrap 320 wide, padding 18, 1px border→ .es-qr-image-wrap
 *   .oc-qr-canvas     282x282  (= 320 − 2×18 − 2×1)    → .es-qr-image
 *   .oc-lpa           padding 16px 18px                → .es-lpa-wrap
 * The 282 is DERIVED, not typed: the QR fills the white card's content box, so
 * the image is sized by the wrap rather than by a width of its own. The PNG EME
 * writes is 320x320, declared as the intrinsic size on the element so the box is
 * reserved before decode.
 * ─────────────────────────────────────────────────────────────────────────── */

/* Hero: full-bleed tinted band. The `.es-main` root gutter already supplies the
   content rail, so the band's own inline padding stays 0 (ESM-17). */
.es-oc-hero {
  background: linear-gradient(180deg, var(--color-accent-1-soft) 0%, var(--color-base) 100%);
  padding: 56px 0 28px;
  margin-bottom: 4px;
}
.es-oc-hero-inner { display: flex; align-items: center; gap: 24px; }
.es-oc-tick {
  width: 64px; height: 64px; border-radius: 50%;
  background: var(--color-accent-1); color: #FFFFFF;
  display: grid; place-items: center; flex: 0 0 auto;
  box-shadow: 0 8px 24px rgba(53, 116, 93, 0.30);
}
.es-oc-h1 {
  font-size: clamp(2rem, 1.4rem + 1.8vw, 2.75rem);
  font-weight: 600; letter-spacing: -0.03em; line-height: 1.1; margin: 0;
}
.es-oc-hero-sub { color: var(--color-contrast-muted); margin: 8px 0 0; font-size: 16px; }
.es-oc-hero-meta {
  display: flex; gap: 18px; flex-wrap: wrap; margin-top: 18px;
  font-size: 13px; color: var(--color-contrast-muted);
}
.es-oc-hero-meta strong { color: var(--color-contrast); font-family: var(--font-family-mono); font-weight: 600; }

.es-oc-note { margin: 24px 0; }
.es-oc-timing { margin: 0 0 28px; }

/* ESM-58 — the terminal-state callout's exit links ("Check again", "Open My
 * eSIMs"). They sit inside `.es-callout-body`, which is a plain text flow, so
 * they need their own row rather than trailing the sentence mid-paragraph. */
.es-oc-terminal-actions {
  display: flex;
  gap: 18px;
  flex-wrap: wrap;
  margin-top: 10px;
}
.es-oc-terminal-actions .es-btn-link { color: inherit; text-decoration: underline; text-underline-offset: 3px; }

.es-oc-qr-section { margin-bottom: 28px; }
.es-oc-qr-heading { font-size: 24px; font-weight: 600; letter-spacing: -0.025em; margin: 8px 0 22px; }

.es-qr-card {
  padding: 32px;
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-xl);
  margin-bottom: 18px;
}
.es-qr-grid { display: grid; grid-template-columns: 320px 1fr; gap: 32px; align-items: start; }
.es-qr-image-wrap {
  background: #FFFFFF;
  padding: 18px;
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-lg);
  display: flex; flex-direction: column; align-items: center;
}
/* width:100% + aspect-ratio, NOT a hard 282px: the number is the wrap's content
   box, so if the rail ever changes the QR stays square and stays inside it. */
.es-qr-image { display: block; width: 100%; height: auto; aspect-ratio: 1; background: #FFFFFF; }
.es-qr-caption {
  font-family: var(--font-family-mono); font-size: 11px; letter-spacing: 0.06em;
  color: var(--color-contrast-faint); margin-top: 14px; text-align: center;
}
.es-qr-save-row { display: flex; gap: 8px; margin-top: 14px; width: 100%; }
.es-qr-save-row .es-btn { flex: 1; justify-content: center; }
.es-qr-nofile { font-size: 13px; color: var(--color-contrast-muted); margin: 0; text-align: center; }

.es-qr-body-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
.es-qr-country { font-family: var(--font-family-heading); font-weight: 700; font-size: 26px; letter-spacing: -0.025em; }
.es-qr-plan { color: var(--color-contrast-muted); font-size: 14.5px; margin-top: 2px; }

.es-lpa-wrap {
  margin-top: 22px; padding: 16px 18px;
  background: var(--color-base);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-md);
}
.es-lpa-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
.es-lpa-label {
  font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--color-contrast-faint); font-weight: 600;
}
.es-lpa-copy {
  font: inherit; font-size: 12px; font-weight: 600;
  background: var(--color-contrast); color: var(--color-base);
  padding: 5px 10px 5px 8px; border-radius: var(--radius-full); border: 0;
  cursor: pointer; display: inline-flex; align-items: center; gap: 5px;
}
.es-lpa-copy.is-copied { background: var(--color-success); }
/* The LPA string is a bare mono block inside `.es-lpa-wrap`'s own card — it must
   NOT inherit the boxed `.es-mono-code` treatment, or the card is drawn twice. */
.es-lpa-string {
  font-family: var(--font-family-mono); font-size: 13.5px;
  word-break: break-all; line-height: 1.55; color: var(--color-contrast);
}
.es-qr-iccid {
  display: flex; gap: 14px; flex-wrap: wrap; margin-top: 12px;
  font-family: var(--font-family-mono); font-size: 12.5px; color: var(--color-contrast-muted);
}
.es-qr-iccid strong { color: var(--color-contrast); font-weight: 600; }

/* ═══════════════════════════════════════════════════════════════════════════
   ORDER RECORD — the WooCommerce half of the confirmation page (ESM-63)
   ═══════════════════════════════════════════════════════════════════════════
   The mock (order-confirmation.html) has no counterpart for this section: it
   ends at `.oc-next-grid` and never draws an order summary, a totals table or a
   billing address. WooCommerce contributes all three, and until now the page
   simply printed them raw — bold `<dt>`-style labels, a bare bordered table and
   an `<address>` in the UA default, directly under a QR card with 32px padding
   and an 1.75rem radius. Two halves of one page in two visual languages, and
   the seam landed on the LAST screen of the purchase.

   The blocks themselves are untouched — they hold the real order data and are
   the correct renderers for it. What is added is the page's own card treatment,
   borrowed from `.es-qr-card` / `.oc-next-card` so the bottom of the page is
   recognisably the same document as the top.

   The `order-confirmation-summary` block that used to sit above these is gone
   from templates/order-confirmation.html — see patterns/qr-display.php for why
   removing it beats restyling it (three of its five rows were already in the
   hero, and its `:nth-child` positions are not stable).
   ═════════════════════════════════════════════════════════════════════════ */
.es-oc-record { display: flex; flex-direction: column; gap: 18px; margin-top: 28px; }

.es-oc-record > .wc-block-order-confirmation-totals-wrapper,
.es-oc-record > .wc-block-order-confirmation-billing-wrapper,
.es-oc-record > .wc-block-order-confirmation-shipping-wrapper,
.es-oc-record > .wc-block-order-confirmation-additional-fields-wrapper,
.es-oc-record > .wc-block-order-confirmation-additional-information {
  margin: 0;
  padding: 28px 32px;
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-xl);
}

/* `!important` is load-bearing, not laziness. The heading comes from the
   WooCommerce-registered pattern `woocommerce/order-confirmation-totals-heading`,
   which ships an INLINE `style="font-size:clamp(15.747px, …, 24px)"` on the
   <h2>. An inline declaration outranks any selector we can write, so without
   this the card heading stays on core's fluid ramp and reads 16px on a phone
   and 24px on desktop — the drift Kai measured. 18px is the design's secondary
   card heading (`.oc-next-card h3` 17px, `.oc-qr-country` 26px): this section
   is a record, not a headline. */
.es-oc-record > div > h2.wp-block-heading {
  font-size: 18px !important;
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1.3;
  margin: 0 0 16px;
  color: var(--color-contrast);
}

.es-oc-record table.wc-block-order-confirmation-totals__table {
  width: 100%;
  border: 0;
  border-collapse: collapse;
  font-size: 14.5px;
}
/* The column header row is a label, not content — same eyebrow treatment as
   `.es-lpa-label`, so the row of real data below it is what carries weight. */
.es-oc-record .wc-block-order-confirmation-totals__table thead th {
  padding: 0 0 10px;
  border: 0;
  border-bottom: 1px solid var(--color-neutral);
  text-align: left;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-contrast-faint);
}
.es-oc-record .wc-block-order-confirmation-totals__table thead th.wc-block-order-confirmation-totals__total,
.es-oc-record .wc-block-order-confirmation-totals__table td.wc-block-order-confirmation-totals__total,
.es-oc-record .wc-block-order-confirmation-totals__table tfoot th { text-align: right; }
.es-oc-record .wc-block-order-confirmation-totals__table tbody td {
  padding: 14px 0;
  border: 0;
  border-bottom: 1px solid var(--color-neutral);
  vertical-align: top;
}
.es-oc-record .wc-block-order-confirmation-totals__table tbody td a {
  color: var(--color-contrast);
  font-weight: 600;
  text-decoration: none;
}
.es-oc-record .wc-block-order-confirmation-totals__table tbody td a:hover { text-decoration: underline; }
.es-oc-record .wc-block-order-confirmation-totals__table .product-quantity {
  color: var(--color-contrast-muted);
  font-family: var(--font-family-mono);
  font-weight: 500;
  font-size: 13px;
}
.es-oc-record .wc-block-order-confirmation-totals__table tfoot th,
.es-oc-record .wc-block-order-confirmation-totals__table tfoot td {
  padding: 14px 0 0;
  border: 0;
  font-weight: 600;
}
/* Money in mono, like every other figure on this page (`.es-oc-hero-meta strong`,
   `.es-qr-iccid`). */
.es-oc-record .woocommerce-Price-amount {
  font-family: var(--font-family-mono);
  font-weight: 600;
  color: var(--color-contrast);
}

.es-oc-record .wc-block-order-confirmation-billing-address address,
.es-oc-record .wc-block-order-confirmation-shipping-address address {
  margin: 0;
  padding: 0;
  border: 0;
  font-style: normal;
  font-size: 14.5px;
  line-height: 1.65;
  color: var(--color-contrast-muted);
}

/* src: order-confirmation.html @1023.98 — the QR grid stacks and the white card
   centres at its natural 320. */
@media (max-width: 1023.98px) {
  .es-qr-grid { grid-template-columns: 1fr; }
  .es-qr-image-wrap { max-width: 320px; margin: 0 auto; }
}
@media (max-width: 767.98px) {
  .es-oc-hero-inner { flex-direction: column; align-items: flex-start; }
  .es-qr-card { padding: 22px; }
  .es-oc-record > .wc-block-order-confirmation-totals-wrapper,
  .es-oc-record > .wc-block-order-confirmation-billing-wrapper,
  .es-oc-record > .wc-block-order-confirmation-shipping-wrapper,
  .es-oc-record > .wc-block-order-confirmation-additional-fields-wrapper,
  .es-oc-record > .wc-block-order-confirmation-additional-information { padding: 22px; }
}

/* ─── Small shared pieces ─────────────────────────────────────────────────── */
.es-empty-state { padding: 48px 32px; text-align: center; color: var(--color-contrast-muted); background: var(--color-base-elevated); border: 1px solid var(--color-neutral); border-radius: var(--radius-lg); }

/* ESM-82 — an empty state's call to action stands off its explanation.
 *
 * Measured on /my-account/my-esims/ @1440 for a customer with no eSIMs: the
 * paragraph's bottom and `.wp-block-buttons`' top were both at y=350 — a gap of
 * exactly 0. (The 14px of apparent air was the doubled button wrapper's own
 * padding, fixed in patterns/my-esims-list.php in this release.)
 *
 * SWEEP — four surfaces render `.es-empty-state`, and two of them already had
 * this right, which is where the 24px comes from rather than from taste:
 *   · patterns/cart-empty.php      `.es-cart-empty-actions`  margin-top 24px  ✓
 *   · patterns/search-results.php  `.es-sr-empty-actions`    margin 1.5rem    ✓
 *   · patterns/my-esims-list.php   `.wp-block-buttons`       nothing          ← reported
 *   · patterns/plan-comparison.php a bare <a> after a <p>    nothing          ← same class,
 *     not reported because it only renders for a country with zero plans, which
 *     no country currently is. Fixed anyway: it is the same defect waiting for
 *     the first empty destination.
 * Stated on the shared component, not on either page, so a fifth empty state
 * inherits the spacing instead of re-deriving it. */
.es-empty-state .wp-block-buttons,
.es-plan-comparison-empty .es-btn {
  margin-top: 24px;
}
/* The block LAYOUT classes never arrive on this markup, so the layout has to be
 * stated. `patterns/my-esims-list.php` is echoed as raw HTML by WooCommerce's
 * `woocommerce_account_my-esims_endpoint` hook — its `<!-- wp:buttons -->`
 * comments are still literally in the served DOM, i.e. `do_blocks()` never ran —
 * so `is-layout-flex` / `is-content-justification-center` are absent and
 * `.wp-block-button` is a plain block div. Until now the `es-btn` class removed
 * in ESM-82 was accidentally covering for that with its own
 * `display: inline-flex`; taking the class away left the wrapper a full-width
 * 785.8x24 block with a 221.3x49 inline anchor spilling out of it. Measured
 * both states on the deployed page. This is what the block renderer would have
 * emitted, said once, on the shared component. */
.es-empty-state .wp-block-buttons {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px;
}
.es-empty-state .wp-block-button { display: inline-flex; }
/* Same root cause, second symptom, fixed in theme.json rather than here — see
 * `styles.elements.button.border.radius`. The pill radius was coming from CORE's
 * per-block stylesheet (`wp-block-button`, `border-radius: 9999px`), which
 * WordPress enqueues only when it has PARSED a `core/button` block on the page.
 * On this raw-echoed markup it never does, so the CTA rendered as a 221x52
 * SQUARE while the identical button on the homepage was a pill. Declaring the
 * radius on the button ELEMENT in theme.json makes it independent of whether
 * core's conditional stylesheet happens to load, which is the actual fragility. */
/* `.es-plan-comparison-empty`'s CTA is an inline <a>; a top margin on an inline
 * box is ignored, so it has to become inline-block first. `.es-btn` already
 * declares `display: inline-flex`, which is inline-level and DOES take the
 * margin — this is the guard against a future variant that does not. */
.es-plan-comparison-empty .es-btn { display: inline-flex; }

/* ESM-82 structural guard — a `core/button` wrapper is a LAYOUT box, never a
 * control.
 *
 * `<!-- wp:button {"className":"es-btn-cta"} -->` puts the class on
 * `.wp-block-button`, the <div>, while the thing a visitor clicks is the <a>
 * inside it. Both then get painted, and the result is one pill drawn around
 * another — found on two surfaces at once (My eSIMs empty state, homepage
 * "View all destinations"). Both call sites are corrected; this rule is what
 * stops the next one shipping silently. The wrapper loses the control paint and
 * the <a> keeps theme.json's button element styling, so the worst outcome of
 * repeating the mistake is a button that looks like the default CTA — not two
 * boxes. */
.wp-block-button[class*="es-btn"] {
  background: none;
  border: 0;
  padding: 0;
  min-height: 0;
  box-shadow: none;
}
/* ─── Empty cart (ESM-53) ─────────────────────────────────────────────────
 * Uses the shared `.es-empty-state` card; only the inner rhythm is page-local.
 * `.wp-block-woocommerce-empty-cart-block` gets its bottom margin dropped
 * because the card now IS the section — WooCommerce's own spacing was tuned for
 * a heading + separator + product grid that no longer renders.
 * ───────────────────────────────────────────────────────────────────────── */
.wp-block-woocommerce-empty-cart-block { margin-block: 0; }
.es-cart-empty-state { padding: 56px 32px; }
.es-cart-empty-title {
  font-size: 26px; font-weight: 600; letter-spacing: -0.025em; line-height: 1.2;
  color: var(--color-contrast); margin: 10px 0 0;
}
.es-cart-empty-lead { max-width: 34rem; margin: 12px auto 0; font-size: 15px; line-height: 1.6; }
.es-cart-empty-actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 24px; }
@media (max-width: 575.98px) {
  .es-cart-empty-state { padding: 40px 20px; }
  .es-cart-empty-title { font-size: 22px; }
  .es-cart-empty-actions .es-btn { width: 100%; }
}

.es-coverage-empty { padding: 20px 28px; }
.es-404 { display: grid; place-items: center; padding: 96px 0; text-align: center; }
.es-404-icon { font-size: 48px; line-height: 1; margin-bottom: 16px; }
.es-404-content { max-width: 480px; }
/* ESM-70: `.es-btn-full` deleted — an unused utility; the two places a button
   does go full width set it themselves. */
.es-btn-sm { font-size: 13px; padding: 8px 14px; min-height: 36px; }

/* ═══════════════════════════════════════════════════════════════════════════
   CONTENT RAIL — ESM-17 / KNOWN-RED R3
   ═══════════════════════════════════════════════════════════════════════════
   The design container is `max-width: 1280 + 56px inline padding` → a painted
   content rail of x=136 w=1168 at 1440, x=32 w=704 at 768, x=20 w=335 at 375.
   Before this, theme.json declared contentSize 80rem (1280) with NO root
   padding, so every constrained block landed on a 1280 rail at x=80. Only the
   two patterns that carried a literal `.es-container` div (home hero, PDP hero)
   were on the right rail — everything else was 56px out, and /shop/ had no
   `.es-container` node at all.

   The rail now lives in theme.json where it belongs:
     settings.layout.contentSize = 73rem  (1168px — the PAINTED rail)
     styles.spacing.padding.left/right = var(--container-gutter)  (56/32/20px)
     settings.useRootPaddingAwareAlignments = true  (already on)
   so every constrained block on every template — including the WC Cart and
   Checkout blocks that arrive from page post_content — is on the rail with no
   per-template wrapper. `.es-container` divs that remain in ported patterns are
   neutralised below — otherwise they would apply the gutter twice (x=192).

   Full-bleed bands (header, footer) must escape the root padding, which is what
   `.alignfull` does for block children. Template PARTS are not alignable, so the
   same negative margin is applied to them here.
   ═════════════════════════════════════════════════════════════════════════ */
.wp-site-blocks > .wp-block-template-part {
  padding-left: 0;
  padding-right: 0;
}

/* Defensive: if a design pattern is ever re-introduced with an `.es-container`
   wrapper, it must not re-apply the gutter inside an already-railed block tree. */
.wp-site-blocks .es-container {
  max-width: var(--wp--style--global--content-size);
  padding-left: 0;
  padding-right: 0;
  margin-left: auto;
  margin-right: auto;
}

/* ─── Round-2 corrections (measured against live after the first R1/R3 deploy) ─ */

/* Cart line rows — src: cart.html .ct-item 665x146.3, padding 20; thumbnail
 * .ct-item-flag-wrap 88x62. WC renders the cart as a <table>, and padding on a
 * <tr> is inert, so the row padding has to go on the cells. */
.wc-block-cart-items__row > td { padding-top: 20px; padding-bottom: 20px; vertical-align: top; }
.wc-block-cart__main { padding-right: 0; }
.wc-block-cart-item__image { width: 88px; }
.wc-block-cart-item__image img { width: 88px; height: 62px; object-fit: cover; border-radius: var(--radius-md); }

/* Install section heading — src: japan.html .pd-install h2 32px / -0.03em. */
.es-install-stepper h2 { font-size: 32px; font-weight: 600; letter-spacing: -0.03em; line-height: 1.15; margin: 0 0 24px; color: var(--color-base); }
/* The stepper is shared by three surfaces and the design gives it a different
 * heading size on each: japan.html `.pd-install h2` is 32px (it is that page's
 * closing feature block), order-confirmation.html `.oc-installer-head h2` is
 * 28px (it sits below the QR cards, which already carry the page's emphasis).
 * 32 is the default above; this is the confirmation page's own step down. */
.es-order-confirmation .es-install-stepper h2 { font-size: 28px; }
.es-install-stepper p { color: rgba(251, 247, 240, 0.7); }

/* Step rows — src: .pd-install-step 51px: a 26px numeral + 12px padding each
 * side. The title is INLINE with the description in the design (one text line),
 * not a stacked <h4> + <p> pair, which is what made our rows 68px. */
.es-step-title { display: inline; font-size: 14px; font-weight: 600; }
.es-step-desc  { display: inline; margin: 0; }
.es-step-title::after { content: " "; }

/* ─── Round-3 corrections ─────────────────────────────────────────────────── */

/* MOBILE HEADER OVERFLOW (KNOWN-RED R4, header share).
 * `.es-header-actions` is `nowrap` and the search field carries min-width 240,
 * so at 375 the actions group measured 356px wide and pushed the document to
 * 641px — 266px of sideways scroll on every page. The design solves this by
 * hiding the nav and the header search below the desktop breakpoint
 * (components.css `@media (max-width: 1023.98px) { .es-nav, .es-hdr-search
 * { display: none } }`); WP's Navigation block only swaps to its overlay at
 * 600px, so the same swap is forced here for the 600–1024 band. */
.es-header-inner { flex-wrap: wrap; }
.es-header-actions { flex-wrap: wrap; }

/* ESM-54 — the mobile search entry point.
 *
 * `.es-header-search` is hidden below 1024 (its 240px pill does not fit beside
 * the logo, the hamburger, the account icon and the cart at 375), and until now
 * nothing replaced it: measured live 2026-07-31, /shop/ at 768 and 375 carried
 * ZERO visible search inputs, and the homepage hero was the only search field on
 * the entire site below 1024. Search is the only route to the 16 countries
 * missing from the catalog archive (ESM-55), so on a phone those countries were
 * unreachable from every page except the home page.
 *
 * The fix is the design's own answer: `header-footer.jsx` MobileHeaderOpen puts
 * a `.es-drawer-search` field at the top of the drawer. `wp:search` is therefore
 * an inner block of `wp:navigation`, which renders it inside the responsive
 * overlay — the same element core already opens, focus-traps and closes. It is
 * hidden at desktop, where `.es-header-search` is the visible one; exactly one
 * of the two is ever on screen.
 *
 * Selector note: core does NOT wrap a non-navigation inner block in
 * `<li class="wp-block-navigation-item">`. It hoists the form out of the
 * `<ul>` and renders it as a SIBLING inside
 * `.wp-block-navigation__responsive-container-content`, so an `li:has(…)` rule
 * matches nothing and the field was on screen twice at desktop (header height
 * 93 → 159). Verified against the served markup, not assumed. */
.es-nav-search { display: none; }

/* ESM-104 — the nav→drawer breakpoint is 1147.98, not 1023.98.
 *
 * Everything from here to the end of this block is the header's mobile form,
 * and its boundary is the ONLY thing that fixes the 1024–1147 band. ESM-102
 * brought the desktop bar's wrap threshold down to 1147 by re-proportioning
 * gap, nav padding and the search pill; below that no combination of those
 * three keeps five items on one line, and the bar wraps to two rows. Andrew's
 * call, 2026-08-25: hand that band the drawer.
 *
 * The number is 1147.98 and not 1148 because it must be the exact complement of
 * the measured threshold — at 1148 the bar is one row, at 1147 it is not, so
 * the drawer has to take over at 1147.98 and not a pixel earlier. If ESM-102's
 * three values are ever changed again, re-measure the threshold and move this
 * with it: the two numbers are one decision written in two files, and a gap
 * between them is a band with a broken header, exactly what this closes.
 *
 * What this costs, stated plainly: a laptop between 1024 and 1147 now gets a
 * hamburger where it used to get a (wrapped) bar. Nothing becomes unreachable —
 * the drawer carries the same five links plus its own search field, which the
 * desktop bar's `.es-header-search` hands over to below this width. */
@media (max-width: 1147.98px) {
  .es-header-search { display: none; }
  .es-main-nav .wp-block-navigation__responsive-container:not(.is-menu-open) { display: none; }
  .es-main-nav .wp-block-navigation__responsive-container.is-menu-open { display: flex; }

  /* Side gutter for the drawer's contents. Core's own overlay padding resolves
   * to 0 here (`clamp(1rem, var(--wp--style--root--padding-*), 20rem)` against
   * this theme's root padding), so without this the search pill and the nav rows
   * run edge to edge — measured 375x54 at x=0. Uses the same
   * `--container-gutter` as the header bar, so the drawer's left rail lines up
   * with the logo it replaced. */
  .es-main-nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content {
    padding-left: var(--container-gutter);
    padding-right: var(--container-gutter);
  }

  .es-nav-search {
    display: block;
    width: 100%;
    margin: 0 0 12px;
    padding: 0;
  }
  .es-nav-search .wp-block-search__inside-wrapper {
    background: var(--color-base-elevated);
    border: 1px solid var(--color-neutral);
    border-radius: var(--radius-full);
    padding: 4px 4px 4px 14px;
    min-height: var(--touch-target);
    width: 100%;
    min-width: 0;
    align-items: center;
  }
  .es-nav-search .wp-block-search__input {
    border: 0;
    background: transparent;
    font: inherit;
    font-size: 15px;
    font-weight: 400;
    outline: none;
    padding: 0;
    min-width: 0;
  }
  .es-nav-search .wp-block-search__button {
    background: transparent;
    border: 0;
    padding: 6px 8px;
    color: var(--color-contrast-faint);
    min-width: var(--touch-target);
    min-height: var(--touch-target);
  }

  /* The drawer's nav list, now that it is on screen at all.
   *
   * `.es-main-nav` carries `items-justified-right`/`no-wrap` for the DESKTOP
   * bar, and core reuses the same list inside the overlay — so the links laid
   * out as a 112px right-aligned column against a full-width search field. The
   * design's drawer (`header-footer.jsx` MobileHeaderOpen, `.es-drawer-link`)
   * is a full-width left-aligned stack. Scoped to `.is-menu-open` so the
   * desktop bar is untouched.
   *
   * 44px rows: `--touch-target`, the same floor the hamburger was raised to in
   * ESM-40. These rows were never reachable before, so this is the first time
   * the size has been anyone's problem. */
  .es-main-nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__container {
    align-self: stretch;
    align-items: stretch;
    width: 100%;
    gap: 2px;
  }
  .es-main-nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation-item__content {
    display: flex;
    align-items: center;
    width: 100%;
    min-height: var(--touch-target);
    padding: 10px 0;
    border-radius: var(--radius-md);
    font-size: 16px;
  }
  /* ESM-40 / ESM-KAI-32 — the hamburger's hit box was 24x24, exactly the WCAG
   * 2.2 SC 2.5.8 floor and well under the 44px this theme already carries as
   * `--touch-target`. The SVG stays 24px; only the tappable box grows, so the
   * header does not change height (the button is centred in a 92.5px bar). */
  .es-main-nav .wp-block-navigation__responsive-container-open:not(.always-shown) {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: var(--touch-target);
    min-height: var(--touch-target);
  }
}

/* ESM-104, second half. The drawer was drawn for a phone, and widening its band
 * to 1147 put a phone layout on a laptop: measured at 1147 the search pill
 * stretched to 961px and the five links sat in a ~200px column against 900px of
 * empty white. It read as a broken page, not as a menu.
 *
 * Above phone widths the drawer is a PANEL, not a page — the content column is
 * capped and stays on the same left rail as the logo it replaced. Below 768 the
 * cap does not apply and the phone drawer is untouched, which is the whole
 * point of scoping it here rather than editing the rules above. */
@media (min-width: 768px) and (max-width: 1147.98px) {
  .es-main-nav .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content {
    max-width: calc(420px + 2 * var(--container-gutter));
  }
}

/* PDP summary stats — src: japan.html .pd-summary-item 190.2x108.5. The three
 * cards are a fixed-height row in the design (its own labels wrap to two lines);
 * pin the height so the row stays even regardless of label length. */
.es-pdp-stat { min-height: 108.5px; }

/* Order-summary heading — src: cart.html "Order summary" at 20px. WC ships it
 * as a 13px uppercase micro-title. */
.wc-block-cart__totals-title,
.wc-block-components-checkout-order-summary__title-text,
.wc-block-components-checkout-order-summary__title button {
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.02em;
  text-transform: none;
}

/* Place order — src: checkout.html .co-place-btn 607x56 (padding 16). The CART
 * CTA is 50.5 (.ct-checkout-btn); they are different buttons, so they cannot
 * share one min-height. */
.wc-block-components-checkout-place-order-button { min-height: 56px; padding: 16px; }
.wc-block-cart__submit-button { min-height: 50.5px; }

/* CART LINE ITEMS — de-table.
 * src: cart.html .ct-item 665x146.3 (padding 20) with an 88x62 thumbnail.
 * WC renders the cart as a real <table>: padding on the <tr> is inert and the
 * <td>s stretch to the row height, so the thumbnail cell measured 88x132.7
 * instead of 88x62. Turning the row into a flex card is the recipe that has
 * survived production on this lineage (memory: wc-blocks-cart-line-styling).
 * WC also puts a ~53px padding-right on `.wc-block-components-main`, which is
 * what kept the items column at 612 inside a 665 main. */
.wc-block-components-sidebar-layout .wc-block-cart__main { padding-right: 0; }
.es-cart-page .wc-block-cart-items { display: block; width: 100%; border: 0; }
.es-cart-page .wc-block-cart-items thead { display: none; }
.es-cart-page .wc-block-cart-items tbody { display: block; }

/* ESM-26 — the line is a CARD, not a table row with a rule under it.
 * src: cart.html `.ct-item` — grid 88px/1fr/auto, gap 20, padding 20,
 * `--color-base-elevated` on a 1px `--color-neutral` border, radius lg,
 * align-items:center, siblings separated by `.ct-items` gap 14.
 * The previous port stopped at `padding: 20px 0` + a bottom hairline: it had
 * de-tabled the row but never actually drawn the card, which is most of why the
 * line measured short. */
.es-cart-page .wc-block-cart-items__row {
  display: flex;
  align-items: center;
  gap: 20px;
  width: 100%;
  padding: 20px;
  background: var(--color-base-elevated);
  border: 1px solid var(--color-neutral);
  border-radius: var(--radius-lg);
  margin-bottom: 14px;
}
.es-cart-page .wc-block-cart-items__row:last-child { margin-bottom: 0; }
.es-cart-page .wc-block-cart-items__row > td { display: block; padding: 0 !important; border: 0; }
.es-cart-page .wc-block-cart-item__image { flex: 0 0 88px; width: 88px; height: 62px; }
.es-cart-page .wc-block-cart-item__image img { width: 88px; height: 62px; object-fit: cover; border-radius: var(--radius-md); }
.es-cart-page .wc-block-cart-item__product { flex: 1 1 auto; min-width: 0; }
.es-cart-page .wc-block-cart-item__total { flex: 0 0 auto; text-align: right; }

/* Type scale + the vertical rhythm inside the product column.
 *
 * SPECIFICITY, and it is not optional. WooCommerce ships every cart-item rule
 * scoped as `table.wc-block-cart-items .wc-block-cart-items__row .X` — an
 * element + three classes, 0,3,1. The obvious theme override
 * `.es-cart-page .wc-block-cart-item__prices { display: none }` is 0,2,0 and
 * LOSES, silently: the declaration is in the served stylesheet, the selector
 * matches the element, and the property never takes. Measured here 2026-07-30 —
 * three of the rules below (product-name font-size, prices display, wrap gap)
 * were deployed and inert, which reads exactly like a stale-CSS problem and is
 * not one. Every rule that touches a node INSIDE the row therefore repeats WC's
 * own scope with `.es-cart-page` in front (0,4,1). No `!important` anywhere:
 * the cascade is winnable, it just has to be entered properly.
 *
 * The design uses explicit, unequal offsets (sub +4, control row +12) where WC
 * uses one uniform 4px flex gap, so the gap is zeroed and the offsets stated as
 * margins — otherwise every spacing below the title is 4px out.
 * src: .ct-item-title 18px/27, .ct-item-sub 13.5px/20.3 (+4),
 *      .ct-item-actions 38 high (+12), .ct-item-price 22px/33 weight 700. */
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-cart-item__wrap { gap: 0; }
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-components-product-name {
  font-size: 18px; font-weight: 600; letter-spacing: -0.02em;
  line-height: 1.5; color: var(--color-contrast);
}
/* WC prints the UNIT price here and the LINE TOTAL in the right-hand cell. At
 * this store's quantities they are the same string twice, and the design draws
 * exactly one price per line (right column). The totals panel and the checkout
 * review both still carry the full breakdown. */
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-cart-item__prices { display: none; }
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-components-product-metadata { margin-top: 4px; }
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-components-product-metadata,
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-components-product-metadata p {
  font-size: 13.5px; line-height: 1.5; color: var(--color-contrast-muted);
}
/* The qty stepper + remove control ARE this row's `.ct-item-actions`; the design
 * puts that row in the right-hand column, WooCommerce renders it in the product
 * column and CSS cannot re-parent across cells. Same controls, same 38px pill
 * height, one column to the left. */
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-cart-item__quantity { margin-top: 12px; align-items: center; gap: 10px; }
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-components-quantity-selector { min-height: 38px; border-radius: var(--radius-full); }
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-components-quantity-selector__input,
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-components-quantity-selector__button { min-height: 36px; }
/* ESM-77 — the bin glyph used to hang out of its own pill, on EVERY cart row.
 *
 * Measured on the deployed site 2026-08-06 @1440: button box x=274 w=26, glyph
 * box x=287 w=24 — the glyph's right edge at 311 against a pill ending at 300,
 * 11px outside, on both the multi-country row and the country row below it. It
 * is not a consequence of the missing thumbnail; it was simply never seen,
 * because both rows have it and the eye reads it as "an icon next to a pill".
 *
 * Cause is a specificity TIE, not a missing rule. WooCommerce declares the
 * control's whole geometry — `width:24px; height:24px; padding:0; border:0;
 * display:inline` — at
 *   `table.wc-block-cart-items .wc-block-cart-items__row
 *    .wc-block-cart-item__quantity .wc-block-cart-item__remove-link`   (0,4,1)
 * and the theme's override was ALSO (0,4,1): it swapped WC's
 * `.wc-block-cart-item__quantity` for `.es-cart-page`. Equal weight, so only
 * load order decided it, and it decided PER PROPERTY: the theme's `padding` and
 * `border` landed, its (absent) `width` did not. A 24px-wide control was given
 * 24px of horizontal padding and a 1px border, and the glyph had nowhere to go.
 *
 * The scope below now repeats WooCommerce's own selector with `.es-cart-page`
 * in front (0,5,1), and states every box property the control needs instead of
 * inheriting half of them from a rule it happens to outweigh. 38px square is the
 * quantity stepper's pill height, so the two controls are one row.
 * src: cart.html `.ct-item-actions` 38 high. */
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-cart-item__quantity .wc-block-cart-item__remove-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 38px;
  height: 38px;
  min-height: 38px;
  padding: 0;
  border-radius: var(--radius-full);
  border: 1px solid var(--color-neutral);
  color: var(--color-contrast-muted);
}
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-cart-item__quantity .wc-block-cart-item__remove-link svg {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
}
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-cart-item__quantity .wc-block-cart-item__remove-link:hover { border-color: var(--color-error); color: var(--color-error); }
.es-cart-page table.wc-block-cart-items .wc-block-cart-items__row .wc-block-cart-item__total .wc-block-components-product-price {
  font-family: var(--font-family-heading);
  font-size: 22px; font-weight: 700; letter-spacing: -0.025em; line-height: 1.5;
}

/* ─── Round-4 corrections ─────────────────────────────────────────────────── */

/* CONTRAST BUG (visible on every page). The trust-bar block ABOVE IN THIS FILE
 * sets `.es-trust-item strong { color: var(--color-contrast) }` — deep navy —
 * and the theme's footer band is that same navy, so the bold half of every
 * footer trust line ("Instant delivery", "No physical SIM", "193+ countries",
 * "Secure checkout") rendered navy-on-navy: invisible. Confirmed in the cart
 * screenshot 2026-07-29. Inside the footer the strong text must inherit the
 * band's colour.
 *
 * ESM-70 corrected two things: the note named components.css as the source of
 * the navy (it never was — the rule is a few hundred lines up, in this file),
 * and the selector list carried `.es-trust-text` / `.es-footer-trust`, which no
 * markup emits. */
.es-site-footer .es-trust-item strong { color: inherit; }

/* (The former `margin-left: 28px` on the sidebar is gone — it stacked on top of
 * the layout's own 28px gap on checkout and pushed the summary 28px past the
 * rail's right edge, to 1332 instead of 1304.) */

/* Cart totals heading — src: cart.html "Order summary" 20px. WC ships a 13px
 * uppercase micro-title; the unscoped rule lost to WC's own specificity. */
.es-cart-page .wc-block-cart__totals-title,
.es-checkout-page .wc-block-components-checkout-order-summary__title {
  font-size: 20px;
  font-weight: 600;
  letter-spacing: -0.02em;
  text-transform: none;
}

/* Payment method row — src: checkout.html .co-pay-option 298.5x57.8 (padding 16). */
.wc-block-components-radio-control__option { min-height: 57.8px; }

/* The cart/checkout columns add up to EXACTLY the 1168 rail (665 + 28 + 475), so
 * a sub-pixel rounding difference is enough to make the flex container wrap and
 * drop the order summary below the form — measured on checkout: sidebar at
 * y=1152 instead of y=210, i.e. a stacked checkout at desktop width. Pin
 * nowrap above the tablet breakpoint; the ≤1023.98 rules above still stack. */
@media (min-width: 1024px) {
  .es-cart-page .wc-block-components-sidebar-layout,
  .es-checkout-page .wc-block-components-sidebar-layout { flex-wrap: nowrap; }
}


/* Country tile is a column: header top, body middle, footer (price) bottom —
 * src: index.html/destinations.html `.es-tile > .es-tile-inner` space-between. */
.es-country-tile { display: flex; flex-direction: column; justify-content: space-between; }
/* ESM-49: 0.75 → 0.90. Same tint-margin arithmetic as `.es-tile-price`. This
   one measured PASS on the four tints that happen to be on page 1 of the
   catalog and FAIL (3.49:1) on Spain's — a reminder that a tile rule has to be
   checked against the worst of the 15 tints, not against whatever rendered. */
.es-tile-validity { font-size: 12px; opacity: 0.9; margin-top: 2px; }

/* ═══════════════════════════════════════════════════════════════════════════
   NO IMAGE → NO IMAGE BLOCK (ESM-32, theme half)
   ═══════════════════════════════════════════════════════════════════════════
   204 regional + 14 global bundles have no product image and never will:
   Keepgo publishes only its own console marketing graphic for them, carrying
   third-party carrier logos, which we will not put on the storefront. Country
   bundles do carry a flag (EME's CountryFlagImporter).

   The plugin already suppresses WooCommerce's grey placeholder on the CLASSIC
   render path (`woocommerce_product_get_image`). Cart, mini-cart and the
   checkout order summary are WC-Blocks: the Store API returns `images: []` and
   the placeholder <img> is produced CLIENT-SIDE by WC's React `ProductImage`
   component —

     const l = e.thumbnail ? { src: e.thumbnail, … }
                           : { src: PLACEHOLDER_IMG_SRC, alt: "", … };

   — so no PHP filter can reach it and this has to be closed in CSS.

   An empty grey frame is worse than no frame: it promises an image that does
   not exist and reads as a half-loaded page. So the image slot is removed, not
   emptied.

   ── ESM-76, 2026-08-06: REMOVING IT WAS ALSO WRONG. ─────────────────────────
   Removing the slot left a hole where the thumbnail belongs and moved the rest
   of the row: measured on /cart/ @1440, a multi-country row's text column began
   at x=157 and the country row directly below it at x=265 — a 108px step
   between two consecutive lines, which is what a human read as "the row is
   broken". The checkout summary kept its 48px gutter, so the descriptions
   stayed on one rail there, but the quantity badge was left floating against
   nothing.

   The slot now carries a TYPE MARK: the same box, filled with `--color-neutral`
   and a globe glyph. This is not a substitute photograph and not invented data —
   it is a category indicator, the way a folder icon indicates a folder. It says
   "this plan is not one country", which is exactly what is true of every product
   that lands here, and it says nothing about the plan that the product page does
   not already say.

   Why there is no picture to use, which is a DECISION and not an omission:
   Import/CountryFlagImporter.php attaches an image only to `bundle_type=country`
   products, because for regional and global bundles the `img` Keepgo publishes is
   a screenshot of Keepgo's own console — a picture of a supplier's admin UI, not
   of anything this store sells. Giving those bundles real region artwork through
   the media library is the better long-term answer and would fix wp-admin too;
   it is an import change, tracked separately.

   The glyph is drawn with `mask-image`, not `background-image`, so its colour is
   `var(--color-contrast-muted)` rather than a hex frozen inside a data URI —
   theme.json stays the only place a colour is chosen.

   THREE THINGS THAT MAKE THE OBVIOUS ONE-LINER WRONG — all measured live
   2026-07-31 before this was written, none of them visible from the markup:

   1. `.wc-block-cart-item__image` renders inside TWO different layouts. On
      /cart/ at desktop the theme has de-tabled the row into a flex card (see
      "CART LINE ITEMS — de-table" above) and `display:none` collapsed it
      cleanly. But WooCommerce re-grids the same row at narrow widths
      (`.is-small|.is-medium|.is-mobile table.wc-block-cart-items
      .wc-block-cart-items__row`, 0,3,1 — it beats the theme's 0,2,0), and the
      mini-cart drawer is ALWAYS that grid. In a grid, `display:none` does not
      collapse a track, it removes the item from auto-placement: `__product` is
      pinned to columns 2–4 but `__total` is auto-placed, so it fell back into
      column 1 and the price landed on top of the product title. Measured:
      $37.82 jumped from x=1374 to x=979 in the drawer, and the same on /cart/
      at 375. That needed the row's track list restated with a zeroed first
      track and `__total` pinned to column 4.

      ESM-76 RETIRES ALL THREE OF THOSE COMPENSATIONS. They existed only to
      survive a missing cell; the cell is present again, carrying the mark, so
      WooCommerce's own track list is correct in both layouts and the price
      cannot be auto-placed anywhere unexpected. Keeping them would now be the
      bug — a zeroed first track would collapse the mark.

   2. WooCommerce nests the QUANTITY badge INSIDE
      `.wc-block-components-order-summary-item__image`, and that badge is the
      ONLY place the checkout states a line's quantity — the description
      carries the unit price, the right column the line total, neither says
      "2". Hiding the wrapper would have deleted that silently. So the checkout
      keeps the wrapper (and its 48px gutter, so an image line and a no-image
      line keep their descriptions on one rail) and only loses the image: the
      badge stays absolutely positioned exactly where it already was. Verified:
      wrap 48px @x=875, badge @x=913, description @x=923 on BOTH lines.

   3. `:has()` is load-bearing here, so the fail-safe direction matters. The
      universal `img[...]` rule below is deliberately a SEPARATE rule: an
      engine without `:has()` drops the rules that use it but keeps that one,
      which degrades to an empty tile rather than back to a grey frame.

   The hook is the placeholder's own URL. `aria-hidden` on the cart cell looked
   like a cleaner signal but is computed as `!objectHasProp(image,'alt') ||
   !image.alt` — it keys off the ALT TEXT, so a real flag imported without alt
   would be hidden too. Silently deleting a real image is a worse failure than
   showing a grey one.
   ═════════════════════════════════════════════════════════════════════════ */

/* Universal: WooCommerce's placeholder never renders on this storefront. Kept
   as its own rule so it survives without `:has()`, and so a WC surface not
   enumerated below degrades to blank space, never to the grey frame. */
img[src*="woocommerce-placeholder"] { display: none; }

/* ── The type mark ─────────────────────────────────────────────────────────
   One declaration block for both surfaces (cart row / mini-cart row, and the
   checkout order summary), because they are the same statement: "this line has
   no picture of a place, because it is not one place".

   `--es-typemark` holds the glyph ONCE. It is applied through `mask-image`, so
   the tile colour and the glyph colour are both tokens — a `background-image`
   data URI would have baked `#5A6478` into a string no palette change can
   reach, which is the ESM-71 defect in a different costume.

   The glyph is a meridian globe at 24×24, stroked at 1.6 and drawn with
   `stroke` only (no fill), so the mask alpha is the outline. */
.wc-block-cart-item__image:has(img[src*="woocommerce-placeholder"]),
.wc-block-components-order-summary-item__image:has(img[src*="woocommerce-placeholder"]) {
  --es-typemark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M3 12h18'/%3E%3Cellipse cx='12' cy='12' rx='4.2' ry='9'/%3E%3C/svg%3E");
  position: relative;
}
/* The TILE is a pseudo-element, not the cell's own background, and that is not
 * a stylistic choice. WooCommerce ships
 *   `table.wc-block-cart-items, table.wc-block-cart-items td, table.wc-block-cart-items th
 *    { background: none !important }`
 * — every background longhand, `!important`. Measured after the first deploy:
 * the glyph painted and the tile did not (`background-color: rgba(0,0,0,0)` on a
 * cell whose `border-radius: 10px` from the SAME rule had applied), which is the
 * signature of exactly one property being overridden. The selector reaches `td`,
 * not `td::before`, so drawing the tile on a pseudo-element sidesteps it without
 * an `!important` of our own — and it makes the cart cell and the checkout
 * summary div, which never had this problem, one code path instead of two.
 * `pointer-events: none` keeps the anchor underneath clickable. */
.wc-block-cart-item__image:has(img[src*="woocommerce-placeholder"])::before,
.wc-block-components-order-summary-item__image:has(img[src*="woocommerce-placeholder"])::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--color-neutral);
  border-radius: var(--radius-md);
  pointer-events: none;
}
.wc-block-cart-item__image:has(img[src*="woocommerce-placeholder"])::after,
.wc-block-components-order-summary-item__image:has(img[src*="woocommerce-placeholder"])::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 24px;
  height: 24px;
  background-color: var(--color-contrast-muted);
  -webkit-mask: var(--es-typemark) center / contain no-repeat;
  mask: var(--es-typemark) center / contain no-repeat;
  pointer-events: none;
}
/* The `<img>` is hidden, so its `<a>` collapsed to 0×0 and the mark was the one
   thumbnail on the page that did not link to its product. Give the anchor the
   cell so the mark is clickable exactly like a flag is. */
.wc-block-cart-item__image:has(img[src*="woocommerce-placeholder"]) > a {
  display: block;
  width: 100%;
  height: 100%;
}
/* Only the GRID layouts need this: in the flex cart the theme already pins the
   cell to 88×62 (see "CART LINE ITEMS — de-table"), and an explicit width+height
   pair makes `aspect-ratio` inert, so this is a no-op there rather than a second
   opinion. In the mini-cart drawer and on the narrow cart the cell has no
   intrinsic height once the image is hidden, and would have collapsed to a
   coloured line. */
table.wc-block-cart-items .wc-block-cart-items__row .wc-block-cart-item__image:has(img[src*="woocommerce-placeholder"]) {
  aspect-ratio: 88 / 62;
  min-height: 42px;
}

/* Checkout order summary. The wrapper is a flex item that otherwise STRETCHES to
   the line's height — measured 84.2px on a two-line description against 56.3 on
   a one-line one — so the mark has to be pinned or it becomes a tall stripe that
   is a different size on every row. 48px square, top-aligned with the
   description, keeping WC's 48px gutter so image lines and mark lines share one
   rail (measured: description x=923 on both). */
.wc-block-components-order-summary .wc-block-components-order-summary-item__image:has(img[src*="woocommerce-placeholder"]) {
  flex: 0 0 48px;
  width: 48px;
  height: 48px;
  align-self: flex-start;
  padding-bottom: 0;   /* the 16px only existed to make room for the image */
}
/* WooCommerce hangs the quantity badge off this wrapper's top-right corner
   (10px outward on both axes, measured). The glyph is centred in a 48px box, so
   it occupies 12…36 on each axis while the badge's footprint is 38…48 × -10…10 —
   verified on the deployed checkout, badge 913…933 × 298.5…318.5 against a glyph
   at 887…911 × 320.5…344.5. No overlap, and nothing clips the badge because the
   tile is a pseudo-element rather than a clipped background. */
