/* ============================================================================
   PEAR - Monochrome "Liquid Glass" theme
   Black & white frosted-glass surfaces · a single pear-green accent
   Fluid hover lift (desktop) + tactile press (mobile/touch) · RTL-aware
   Type: var(--font-ui) everywhere - SF Pro / Heebo (was Inter / Urbanist; see the
   LIQUID GLASS DESIGN SYSTEM layer at the end of this file). The PEAR wordmark alone
   keeps --pear-wordmark.
   NOTE: pure styling + micro-interaction layer. Every existing id / class /
   state hook (.reveal .show .live .show-live .show-result .is-live .is-active
   .is-duo .is-complete .mock) is preserved 1:1 - no logic touched.
   ============================================================================ */

:root {
  /* ── pear-green accent (the only colour) ── */
  --pear:        #1a1a1c;   /* signature pear green - fills / borders / glow */
  --pear-deep:   #0a0a0b;   /* darker pear - text on light glass */
  --pear-soft:   rgba(141, 182, 0, .12);
  --pear-tint:   rgba(141, 182, 0, .16);
  --pear-ring:   rgba(141, 182, 0, .22);
  --pear-glow:   rgba(141, 182, 0, .50);

  /* ── monochrome ink scale ── */
  --ink:        #0a0a0b;   /* headings / strong ink */
  --text:       #1a1a1c;
  --muted:      #6b6b70;
  --muted-2:    #9a9aa0;

  /* ── glass surfaces ── */
  --glass:        rgba(255, 255, 255, .45);
  --glass-strong: rgba(255, 255, 255, .62);
  --glass-soft:   rgba(255, 255, 255, .30);
  --glass-dark:   rgba(16, 16, 18, .62);
  --glass-border: rgba(255, 255, 255, .55);
  --glass-line:   rgba(0, 0, 0, .10);

  --blur:       blur(20px) saturate(180%);
  --blur-lg:    blur(34px) saturate(180%);

  --bg:         #f2f2f3;
  --bg-soft:    rgba(255, 255, 255, .42);

  --radius:     22px;
  --radius-sm:  16px;
  --radius-lg:  26px;
  --pill:       9999px;

  /* ── soft, deep, multi-layered ambient shadows (neutral) ── */
  --shadow:      0 8px 32px rgba(0, 0, 0, .08);
  --shadow-soft: 0 4px 18px rgba(0, 0, 0, .06);
  --shadow-lift: 0 22px 60px rgba(0, 0, 0, .16), 0 6px 18px rgba(0, 0, 0, .08);
  --shadow-pear: 0 12px 36px rgba(141, 182, 0, .26);

  /* signature fluid easing */
  --ease:       cubic-bezier(.16, 1, .3, 1);

  /* ── PEAR brand wordmark - Didot with elegant serif fallbacks. NOT a web font:
     Apple devices render true Didot; elsewhere it falls back to Bodoni MT /
     Cochin / Georgia serif. Applied ONLY to literal "PEAR"/"pear" brand text. */
  --pear-wordmark: 'Didot', 'Didot LT STD', 'Bodoni MT', 'Cochin', 'Georgia', serif;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ── ambient backdrop - soft grey light with a faint pear-green wash ─────── */
body {
  position: relative;
  min-height: 100vh;
  background:
    radial-gradient(1200px 800px at 82% -8%,  rgba(141, 182, 0, .14), transparent 60%),
    radial-gradient(1000px 720px at -6% 12%,  rgba(0, 0, 0, .05),     transparent 58%),
    radial-gradient(900px 900px  at 50% 118%, rgba(141, 182, 0, .08), transparent 60%),
    linear-gradient(180deg, #f7f7f6 0%, #eeeeec 48%, #f3f3f2 100%);
  background-attachment: fixed;
  overflow-x: hidden;
}
/* slow-drifting frosted orbs add organic motion beneath the glass */
body::before,
body::after {
  content: "";
  position: fixed;
  z-index: 0;
  border-radius: 50%;
  filter: blur(60px);
  opacity: .5;
  pointer-events: none;
}
body::before {
  width: 46vmax; height: 46vmax;
  top: -14vmax; inset-inline-end: -12vmax;
  background: radial-gradient(circle at 30% 30%, rgba(141, 182, 0, .42), transparent 65%);
  animation: orbDrift 22s var(--ease) infinite alternate;
}
body::after {
  width: 40vmax; height: 40vmax;
  bottom: -16vmax; inset-inline-start: -10vmax;
  background: radial-gradient(circle at 60% 40%, rgba(0, 0, 0, .14), transparent 65%);
  animation: orbDrift 28s var(--ease) infinite alternate-reverse;
}
@keyframes orbDrift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(-4vmax, 4vmax, 0) scale(1.12); }
}

h1, h2, h3, h4 {
  font-family: var(--font-ui);
  letter-spacing: -.015em;
  color: var(--ink);
}

/* ── screens ───────────────────────────────────────────────────────────── */
.screen { display: none; min-height: 100vh; min-height: 100dvh; position: relative; z-index: 1; }
.screen.active { display: block; animation: fade .5s var(--ease) both; }

/* Returning-device pre-paint gate (Feature 5 fix) - set synchronously by the
   inline script at the top of <body>, BEFORE #screen-calculator's default
   .active state ever paints, whenever this browser has a remembered
   pear_device_id. Force-hides the gate/measurement screen while app.js
   confirms the device server-side; app.js removes the class the instant it
   knows the outcome (see showIdentityGate()/showSizeForm() in app.js). The
   !important beats .screen.active's display:block unconditionally. */
html.pear-returning-check #screen-calculator { display: none !important; }
@keyframes fade { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

/* ── Top-down progressive reveal on screen swap ───────────────────────────
   Prevents the "random element flash" (CLS) of body text/controls popping in
   before the structure that frames them: the outer glass shell/card settles
   first, then the h1/h2 heading, then form controls and copy trail a beat
   behind. Purely opacity + translateY (never affects layout/box size), so it
   cannot itself cause a shift; it composes with .screen.active's own fade
   (each element's animation runs on top of its already-fading ancestor,
   which is what produces the staggered look) and never touches bespoke
   reveals that already exist ( .result-actions.is-ready, .active-garment.is-
   pending, .gd-overlay__panel, etc.) since none of those are h1/h2/form/
   .cam-controls/.calc-shell/.camera-card/.active-garment. */
.screen.active .calc-shell,
.screen.active .camera-card,
.screen.active .active-garment {
  animation: fade .5s var(--ease) both;
}
.screen.active h1,
.screen.active h2 {
  animation: fade .5s var(--ease) both .07s;
}
.screen.active form,
.screen.active .cam-controls {
  animation: fade .5s var(--ease) both .14s;
}
@media (prefers-reduced-motion: reduce) {
  .screen.active .calc-shell,
  .screen.active .camera-card,
  .screen.active .active-garment,
  .screen.active h1,
  .screen.active h2,
  .screen.active form,
  .screen.active .cam-controls { animation: none; }
}

.container {
  max-width: 580px;
  margin: 0 auto;
  padding: 40px clamp(20px, 5vw, 48px);
}

.kicker {
  display: inline-block;
  font-size: .72rem; font-weight: 700; letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--pear-deep);
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur);
  backdrop-filter: var(--blur);
  border: 1px solid var(--glass-border);
  padding: 7px 15px; border-radius: var(--pill);
  margin-bottom: 18px;
  box-shadow: var(--shadow-soft);
}

/* ── step progress ─────────────────────────────────────────────────────── */
.step-progress { max-width: 452px; margin: 0 auto; padding: 24px clamp(18px,5vw,28px) 0; }
.step-progress-label {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: .82rem; font-weight: 600; color: var(--muted); margin-bottom: 12px;
}
.step-progress-label span:last-child { color: var(--pear-deep); font-weight: 800; }
.step-progress-track {
  height: 6px; border-radius: var(--pill); overflow: hidden;
  background: rgba(0, 0, 0, .08);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  border: 1px solid rgba(255, 255, 255, .5);
}
.step-progress-fill {
  height: 100%; width: 0%;
  background: linear-gradient(90deg, var(--pear-deep), var(--pear));
  border-radius: var(--pill);
  box-shadow: 0 0 16px var(--pear-glow);
  transition: width .6s var(--ease);
}

/* ── calculator card (floating frosted-glass pane) ─────────────────────── */
#screen-calculator .container {
  /* Was 460px, capped well under .calc-main's actual ~60%-of-card column
     width - the difference just sat as dead gutter space on both sides
     (most visible on the left, the side away from the RTL reading start).
     Widened to close most of that gap while .calc-main's own padding still
     keeps some breathing room from the card edge. */
  max-width: 560px;
  margin: 16px auto 40px;
  /* Was clamp(40px,6.5vw,64px) - inflated specifically to compensate for
     .calc-card's old align-items:stretch forcing this column to match the
     video's (taller) height, which just left the extra padding piled up as
     dead space below the button instead of closing the gap. .calc-card no
     longer uses stretch (each zone sizes to its own real content, see
     above), so that compensation isn't needed - back to a normal
     generous-but-not-inflated padding. */
  padding: clamp(28px, 4.5vw, 40px) clamp(20px, 3.2vw, 32px);
  background: linear-gradient(135deg, rgba(255,255,255,.45) 0%, rgba(255,255,255,.18) 100%);
  -webkit-backdrop-filter: var(--blur-lg);
  backdrop-filter: var(--blur-lg);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lift);
  position: relative;
  overflow: hidden;
  /* enable the optional 3D parallax tilt (see initCardTilt in app.js) */
  transform-style: preserve-3d;
  transition: transform .5s var(--ease), box-shadow .5s var(--ease);
}
/* glossy top-edge light refraction */
#screen-calculator .container::before {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  border-radius: inherit;
  background: linear-gradient(180deg, rgba(255,255,255,.55), rgba(255,255,255,0) 36%);
  mix-blend-mode: overlay;
}
/* env-mapped specular highlight - follows the pointer (vars set in app.js).
   Radius bumped 420px -> 720px (user likes this effect and wants it to
   reach further/cover more of the card on hover) - same color stops/opacity
   percentages/mix-blend-mode/transition as before, just a bigger circle, so
   the falloff "feel" (how quickly it fades from white core to green wash to
   transparent) scales up proportionally instead of changing character. */
#screen-calculator .container::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  border-radius: inherit; opacity: 0; mix-blend-mode: soft-light;
  background: radial-gradient(
    720px circle at var(--mx, 50%) var(--my, 0%),
    rgba(255, 255, 255, .9) 0%,
    rgba(141, 182, 0, .22) 30%,
    transparent 60%
  );
  transition: opacity .5s var(--ease);
}
#screen-calculator .container.is-tilting::after { opacity: 1; }
#screen-calculator .container > * { position: relative; z-index: 1; }

/* ── ONE blur pass per card, not one per control inside it ──────────────────
   PERF. The rule above gives this card `backdrop-filter: var(--blur-lg)`, and
   per the Filter Effects spec that makes it a BACKDROP ROOT: a descendant's own
   backdrop-filter can only sample what is painted inside this card, never the
   page behind it. What is painted inside is the card's own 135deg white
   gradient plus the ::before sheen - smooth, low-frequency fills. Blurring a
   smooth gradient returns essentially the same smooth gradient, so each of
   these controls was paying for a full backdrop snapshot + blur + its own
   compositing surface to produce no visible pixels.

   That cost is per-control, not per-card: with the fine-tune tray open the size
   form alone puts five blurred inputs, the result box and the primary button on
   screen at once, and it is all recomposited while the card is tilting under
   the pointer. Turning them off leaves ONE real blur pass (the card's own) to
   produce the identical frosted-glass look.

   SCOPED, NOT DELETED - these same classes blur real content elsewhere and must
   keep it: .btn-primary sits over the live camera feed in
   .camera-card__placeholder, and #screen-locked reuses .container/.kicker but
   is NOT a backdrop root (the base .container declares no backdrop-filter), so
   its kicker genuinely blurs the ambient page orbs behind it.

   Verified by pixel diff: all 11 captured scenes (identity gate, size form idle
   / with results + fine-tune tray, card hover with the specular highlight lit
   under the controls, and screen 2, at 1400x900 and 390x844) are byte-identical
   before and after this block at a zero threshold. */
#screen-calculator .container .kicker,
#screen-calculator .container .focus-calc-hint,
#screen-calculator .container .form-group input,
#screen-calculator .container #otpInput,
#screen-calculator .container .result-box,
#screen-calculator .container .btn-primary,
#screen-calculator .container .btn-ghost {
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}

.container h2 { font-size: 1.5rem; font-weight: 800; margin: 0 0 12px; }
.subtitle { color: var(--muted); margin: 0 0 32px; font-size: .9rem; line-height: 1.5; }

/* #identityForm has no gap rule of its own (unlike #sizeForm below), so its
   two .form-group blocks previously sat flush against each other - the main
   driver of the "cramped" identity screen. Same increased gap on both forms
   for a consistent rhythm across the whole panel. */
#identityForm, #sizeForm { display: grid; gap: 28px; }
.form-group { display: grid; gap: 12px; }
.form-group label { font-size: .85rem; font-weight: 600; color: var(--text); }
.required { color: var(--pear-deep); margin-inline-end: 4px; font-weight: 800; }
.optional { color: var(--muted-2); font-weight: 500; font-size: .78rem; }

/* glass-inset inputs with a soft pear-green focus ring */
.form-group input {
  width: 100%; padding: 19px 17px;
  border: 1px solid var(--glass-line); border-radius: var(--pill);
  font-size: .98rem; font-family: inherit; color: var(--ink);
  background: rgba(255, 255, 255, .35);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .05);
  transition: border-color .3s var(--ease), box-shadow .3s var(--ease),
              background .3s var(--ease), transform .3s var(--ease);
}
.form-group input:hover { border-color: rgba(141, 182, 0, .35); background: rgba(255, 255, 255, .5); }
.form-group input:focus {
  outline: none; border-color: var(--pear);
  background: rgba(255, 255, 255, .72);
  box-shadow: 0 0 0 4px var(--pear-ring), inset 0 1px 2px rgba(0, 0, 0, .04);
  transform: translateY(-1px);
}
.form-group input::placeholder { color: var(--muted-2); }
.form-group__label { font-size: .85rem; font-weight: 600; color: var(--text); }

/* ── Gender selector - Liquid Glass segmented switch ──────────────────────────
   Restyled 2026-09-15 from a dark filled pill to the LANGUAGE SWITCH'S glass language
   (.lang-switch): frosted white track, white specular pill, ink for the chosen option and
   muted for the other. That is the design standard this form already lives in - on this
   light surface, white-on-dark text would disappear. Still starts with NEITHER option
   selected (optional field - CLAUDE.md §2.5), so the pill is hidden until a choice.
   Drag physics live in setupGenderSwitch() (app.js), the same model as the language switch:
   tap either half, or press and drag; release snaps on --spring.
   The resting position stays CSS-owned (data-active + dir), so a language flip mirrors the
   pill with no JS. JS only writes an inline transform WHILE a drag is live, then removes it. */
.gender-group { justify-items: center; text-align: center; }
.gender-switch-wrapper {
  display: flex; justify-content: center; align-items: center;
  width: 100%; margin: 0 auto;   /* #sizeForm's 28px grid gap already spaces the groups */
}
.gender-toggle.liquid-glass-track {
  --gs-pad: 4px;
  position: relative; isolation: isolate;
  display: grid; grid-template-columns: 1fr 1fr;
  width: min(290px, 100%); padding: var(--gs-pad);
  border-radius: var(--pill);
  background: linear-gradient(180deg, rgba(255, 255, 255, .46) 0%, rgba(255, 255, 255, .2) 100%);
  -webkit-backdrop-filter: blur(20px) saturate(190%); backdrop-filter: blur(20px) saturate(190%);
  border: 1px solid rgba(255, 255, 255, .62);   /* specular rim */
  box-shadow:
    inset 0 1px 1px rgba(255, 255, 255, .75),   /* top-edge light catch */
    inset 0 -1px 2px rgba(0, 0, 0, .05),
    0 0 0 1px rgba(0, 0, 0, .06),               /* hairline so the rim reads on a light card */
    0 8px 32px rgba(0, 0, 0, .08);
  touch-action: pan-y;   /* a horizontal drag is the switch's; a vertical swipe still scrolls the form */
  -webkit-user-select: none; user-select: none; -webkit-tap-highlight-color: transparent;
}
/* Specular sheen across the upper half of the glass - static, so it costs nothing per frame. */
.gender-toggle.liquid-glass-track::before {
  content: ""; position: absolute; inset: 1px; z-index: 0; border-radius: inherit; pointer-events: none;
  background: linear-gradient(180deg, rgba(255, 255, 255, .38) 0%, rgba(255, 255, 255, 0) 58%);
}

/* The sliding glass pill. Width equals one grid column: 50% of the padding box minus one pad.
   NO backdrop-filter of its own: the track already has one, which makes it the backdrop root -
   a blur on the pill would only ever sample the track's own tint, never the page behind it. */
.liquid-glass-pill {
  position: absolute; z-index: 1; top: var(--gs-pad); bottom: var(--gs-pad); inset-inline-start: var(--gs-pad);
  width: calc(50% - var(--gs-pad));
  border-radius: var(--pill);
  background: linear-gradient(135deg, rgba(255, 255, 255, .97) 0%, rgba(255, 255, 255, .74) 100%);
  border: 1px solid rgba(255, 255, 255, .9);
  box-shadow:
    0 4px 20px rgba(0, 0, 0, .13), 0 1px 3px rgba(0, 0, 0, .08),
    inset 0 1px 0 rgba(255, 255, 255, 1), inset 0 -1px 1px rgba(0, 0, 0, .04);
  opacity: 0; transform: translate3d(0, 0, 0) scale(.9);
  transition: transform .55s var(--spring), opacity .22s var(--ease);
  will-change: transform;
  pointer-events: none;   /* the track owns every pointer - see setupGenderSwitch() */
}
.liquid-glass-track[data-active="men"] .liquid-glass-pill,
.liquid-glass-track[data-active="women"] .liquid-glass-pill { opacity: 1; transform: translate3d(0, 0, 0) scale(1); }
.liquid-glass-track[data-active="women"] .liquid-glass-pill { transform: translate3d(100%, 0, 0) scale(1); }
[dir="rtl"] .liquid-glass-track[data-active="women"] .liquid-glass-pill { transform: translate3d(-100%, 0, 0) scale(1); }
/* Live drag: the pill tracks the pointer 1:1 (inline transform) with no spring lag, and is
   shown even with nothing chosen yet - the spring only plays on the release snap. */
.liquid-glass-track.is-dragging .liquid-glass-pill { transition: opacity .22s var(--ease); opacity: 1; }

.gender-option {
  position: relative; z-index: 2;   /* above the pill */
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 11px 16px; border: 0; border-radius: var(--pill);
  background: transparent; cursor: pointer; font-family: inherit;
  font-size: .9rem; font-weight: 700; line-height: 1; color: var(--muted);
  touch-action: pan-y;
  transition: color .3s var(--ease), text-shadow .3s var(--ease);
}
.gender-option:hover { color: var(--text); }
.gender-option.is-active,
.liquid-glass-track[data-preview="men"] .gender-option[data-gender="men"],
.liquid-glass-track[data-preview="women"] .gender-option[data-gender="women"] {
  color: var(--ink); text-shadow: 0 1px 0 rgba(255, 255, 255, .85);
}
/* While a drag is live the label under the pill leads, not the one the state still names. */
.liquid-glass-track[data-preview="men"] .gender-option[data-gender="women"],
.liquid-glass-track[data-preview="women"] .gender-option[data-gender="men"] { color: var(--muted); text-shadow: none; }
.gender-option:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--pear-ring); }
.gender-option__icon {
  width: 14px; height: 14px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
  opacity: .65; transition: opacity .3s var(--ease), transform .5s var(--spring);
}
.gender-option.is-active .gender-option__icon { opacity: 1; transform: scale(1.08); }

@media (prefers-reduced-motion: reduce) {
  .liquid-glass-pill, .gender-option, .gender-option__icon { transition: none; }
}

.section-divider {
  display: flex; align-items: center; gap: 14px;
  margin: 26px 0 4px; color: var(--pear-deep);
  font-size: .72rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
}
.section-divider::before, .section-divider::after {
  content: ""; flex: 1; height: 1px;
  background: linear-gradient(90deg, transparent, var(--glass-line), transparent);
}

.result-box {
  margin-top: 26px; padding: 18px 20px; border-radius: var(--radius);
  background: linear-gradient(135deg, rgba(141,182,0,.14), rgba(141,182,0,.05));
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  border: 1px solid var(--glass-border);
  box-shadow: var(--shadow-soft);
  text-align: center; opacity: 0; transform: translateY(10px) scale(.98);
  transition: opacity .4s var(--ease), transform .4s var(--ease); pointer-events: none;
}
.result-box.show { opacity: 1; transform: none; }
.result-box.error-result {
  background: rgba(255, 255, 255, .5);
  border-color: rgba(0, 0, 0, .18);
}
#resultLabel { font-size: .86rem; color: var(--muted); margin-bottom: 6px; font-weight: 600; }
.size-display {
  font-family: var(--font-ui); font-size: 2.6rem; font-weight: 900; line-height: 1;
  background: linear-gradient(120deg, var(--pear-deep), var(--pear), var(--pear-deep));
  background-size: 220% 100%;
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
.error-result .size-display { font-size: 1.2rem; -webkit-text-fill-color: var(--ink); color: var(--ink); }

/* ── Out-of-stock notice (#stockNotice, written by renderStockNotice in app.js) ──
   Sits INSIDE .result-box under the size display, so it inherits the box's own
   .show reveal and cannot animate separately from the number it is about.

   [hidden] is honoured explicitly: .result-box's children are otherwise laid out by
   the box, and a UA stylesheet's `display:none` for [hidden] loses to any later
   `display` declaration - so the attribute app.js toggles is made load-bearing here
   rather than relying on nothing else ever setting display on this element.

   Sized in rem and wrapped, never truncated: this is a full sentence naming two
   alternative sizes, and on a 320px phone it runs to four or five lines. Nothing here
   sets a height, so the box simply grows.

   WHAT THAT GROWTH RUNS INTO is .result-actions, which reveals itself by animating
   max-height 0 -> a fixed ceiling with overflow:hidden (see its own rule below). That
   ceiling was measured for label + size display + consent panel + Continue button
   (420px before the consent panel existed); the extra lines fit today but
   with little to spare, and overflow:hidden CLIPS rather than scrolls - it would fail
   silently, on the narrowest screens, in the language with the longest copy. The
   .has-stock-notice rule below raises the ceiling for exactly that case. */
.stock-notice[hidden] { display: none; }
.stock-notice {
  margin: 10px auto 0;
  max-width: 34ch;
  font-size: .82rem;
  line-height: 1.45;
  font-weight: 600;
  color: var(--ink);
  opacity: .85;
  text-wrap: pretty;
  /* The sentence is bidirectional: Hebrew copy with Latin size tokens (L, XL) inside
     it. `plaintext` lets the browser resolve each paragraph's base direction from its
     own first strong character, so the same element reads correctly in both languages
     without app.js having to set dir= per string. */
  unicode-bidi: plaintext;
}
/* A warm amber wash, NOT the error red - the recommendation itself is still correct
   and still shown; only its availability changed. Red here would read as "we could not
   size you", which is the .error-result state and a different message entirely. */
.result-box.has-stock-notice {
  background: linear-gradient(135deg, rgba(214, 158, 46, .16), rgba(214, 158, 46, .05));
  border-color: rgba(214, 158, 46, .38);
}
/* Headroom for the notice's extra lines. Only ever applies when app.js has actually
   painted one, so the shipped reveal is byte-identical for an in-stock recommendation.
   max-height is a CEILING, not a height - a shorter notice still collapses to its own
   content, so this cannot introduce a gap. Wins on SPECIFICITY (three classes vs two),
   not on source order - .result-actions.is-ready is defined much further down this
   file. Both classes are set together by renderStockNotice(). */
.result-actions.is-ready.has-stock-notice { max-height: 1080px; }   /* was 620px; +consent panel, see .result-actions.is-ready */
/* No dark-mode rule here on purpose: this stylesheet has no prefers-color-scheme
   handling anywhere, the room is a fixed light glass theme, and a lone dark override
   on this one element would be the only thing in the file that ever changed. */

/* ── buttons (frosted capsules) ────────────────────────────────────────── */
/* primary action - translucent pear-green glass */
.btn-primary {
  width: 100%; margin-top: 20px; padding: 15px 24px;
  border: 1px solid rgba(141, 182, 0, .35); border-radius: var(--pill);
  background: var(--pear-tint);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--pear-deep);
  font-family: "Urbanist"; font-size: 1rem; font-weight: 800; letter-spacing: .01em;
  cursor: pointer;
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, .55);
  transition: all .4s var(--ease);
}
.btn-primary:hover:not(:disabled) {
  transform: translateY(-2px);
  background: rgba(141, 182, 0, .26); border-color: rgba(141, 182, 0, .55);
  box-shadow: 0 12px 32px rgba(141, 182, 0, .22), inset 0 1px 0 rgba(255, 255, 255, .6);
}
.btn-primary:active:not(:disabled) { transform: scale(.96); }
.btn-primary:focus-visible { outline: none; box-shadow: 0 0 0 4px var(--pear-ring), var(--shadow); }
.btn-primary:disabled {
  background: rgba(255, 255, 255, .3);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--muted-2); border-color: var(--glass-line);
  box-shadow: none; cursor: not-allowed;
}
/* #screen-calculator only (identity/OTP/size-form continue buttons) gets
   extra breathing room before the button - #screen-fitting's startCamBtn
   keeps the base 20px, untouched. */
#screen-calculator .btn-primary { margin-top: 34px; }

.btn-ghost {
  padding: 13px 22px; border: 1px solid var(--glass-border); border-radius: var(--pill);
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--ink); font-weight: 700; font-size: .92rem;
  cursor: pointer; box-shadow: var(--shadow-soft);
  transition: transform .4s var(--ease), box-shadow .4s var(--ease), background .3s var(--ease);
}
.btn-ghost:hover { background: rgba(255, 255, 255, .78); transform: translateY(-3px) scale(1.02); box-shadow: var(--shadow); }
.btn-ghost:active { transform: scale(.96); }

/* ── "Update Measurements" CTA - always-available secondary action on the size
   screen (the user's dashboard). Pear-green outline pill, quieter than the primary
   Continue button but clearly interactive. ── */
.btn-update-measure {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  margin: 16px auto 4px; padding: 11px 22px;
  border: 1px solid rgba(141, 182, 0, .40); border-radius: var(--pill);
  background: var(--pear-soft);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--pear-deep); font-family: "Urbanist"; font-weight: 800;
  font-size: .9rem; letter-spacing: .01em; cursor: pointer;
  box-shadow: var(--shadow-soft), inset 0 1px 0 rgba(255, 255, 255, .5);
  transition: transform .4s var(--ease), background .3s var(--ease), border-color .3s var(--ease), box-shadow .4s var(--ease);
}
.btn-update-measure svg { flex: none; }
.btn-update-measure:hover {
  transform: translateY(-2px);
  background: rgba(141, 182, 0, .20); border-color: rgba(141, 182, 0, .55);
  box-shadow: 0 10px 26px rgba(141, 182, 0, .20), inset 0 1px 0 rgba(255, 255, 255, .55);
}
.btn-update-measure:active { transform: scale(.96); }
.btn-update-measure:focus-visible { outline: none; box-shadow: 0 0 0 4px var(--pear-ring), var(--shadow-soft); }

/* ── shared circular icon badge - used by the "demo already used" screen (see
   #screen-locked in index.html); originally introduced for the 30-day
   re-measure reminder modal, which was replaced by the mandatory monthly
   measurements-refresh form (see PROFILE / MEASUREMENTS section in app.js). ── */
.update-reminder__badge {
  width: 58px; height: 58px; margin: 0 auto 16px;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--pill);
  color: var(--pear-deep);
  background: var(--pear-tint); border: 1px solid rgba(141, 182, 0, .35);
  box-shadow: var(--shadow-pear);
}

/* ── monthly measurements-refresh nudge (Screen 1, forced form) ─────────── */
.measurements-refresh-notice {
  margin: 0 0 16px; padding: 10px 14px;
  border: 1px solid rgba(141, 182, 0, .35); border-radius: 12px;
  background: var(--pear-tint); color: var(--pear-deep);
  font-family: var(--font-ui); font-size: .88rem; font-weight: 600; text-align: center;
}
.measurements-refresh-notice[hidden] { display: none; }

/* ── conditional optional fields - fluid "Liquid Glass" expandable panel ───
   No display:none toggle. The panel is always in the DOM and opens organically
   via max-height + opacity + transform + visibility, so the card stretches
   downward smoothly instead of snapping. app.js toggles .is-expanded. ░░ */
.optional-fields {
  display: grid; gap: 13px;
  max-height: 0; opacity: 0; visibility: hidden;
  overflow: hidden; margin-top: 0;
  transform: translateY(-10px) scale(.985);
  transition:
    max-height .6s cubic-bezier(.25, 1, .5, 1),
    opacity .4s ease-out,
    transform .5s cubic-bezier(.175, .885, .32, 1.1),
    margin-top .5s cubic-bezier(.25, 1, .5, 1),
    visibility 0s linear .6s;          /* stay visible through the collapse, then hide */
}
.optional-fields.is-expanded {
  max-height: 520px; opacity: 1; visibility: visible;
  transform: translateY(0) scale(1); margin-top: 22px;
  transition:
    max-height .6s cubic-bezier(.25, 1, .5, 1),
    opacity .4s ease-out .05s,
    transform .5s cubic-bezier(.175, .885, .32, 1.1),
    margin-top .5s cubic-bezier(.25, 1, .5, 1),
    visibility 0s;
}

/* ── focus-mode calculator hint ────────────────────────────────────────── */
.focus-calc-hint {
  margin-bottom: 22px; padding: 15px 17px; border-radius: var(--radius-sm);
  background: linear-gradient(135deg, rgba(141,182,0,.14), rgba(141,182,0,.05));
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  border: 1px solid var(--glass-border); color: var(--pear-deep);
  font-size: .9rem; font-weight: 600; box-shadow: var(--shadow-soft);
}
.focus-calc-hint[hidden] { display: none; }

/* ── OTP verification screen - same frosted-glass card language as the
   identity gate it's inserted after (Screen 1, first-time visitors only). ── */
.otp-card { display: grid; gap: 13px; }
.otp-card[hidden] { display: none; }
.otp-card h2 { font-size: 1.3rem; font-weight: 800; margin: 0 0 2px; }
#otp-email-hint { color: var(--muted); margin: 0 0 4px; font-size: .9rem; line-height: 1.5; }
#otpInput {
  width: 100%; padding: 16px 17px;
  border: 1px solid var(--glass-line); border-radius: var(--pill);
  font-family: var(--font-ui); font-size: 28px; font-weight: 800;
  letter-spacing: 8px; text-align: center; color: var(--ink);
  background: rgba(255, 255, 255, .35);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, .05);
  transition: border-color .3s var(--ease), box-shadow .3s var(--ease),
              background .3s var(--ease), transform .3s var(--ease);
}
#otpInput::placeholder { letter-spacing: normal; font-size: .95rem; font-weight: 500; color: var(--muted-2); }
#otpInput:hover { border-color: rgba(141, 182, 0, .35); background: rgba(255, 255, 255, .5); }
#otpInput:focus {
  outline: none; border-color: var(--pear);
  background: rgba(255, 255, 255, .72);
  box-shadow: 0 0 0 4px var(--pear-ring), inset 0 1px 2px rgba(0, 0, 0, .04);
  transform: translateY(-1px);
}
#otp-countdown { font-size: .82rem; color: var(--muted); text-align: center; }
#otp-error {
  color: #d13a3a; font-size: .85rem; font-weight: 600; text-align: center; margin: 0;
}
#otp-error[hidden] { display: none; }
#btn-resend-otp { margin-top: 0; width: 100%; }

/* ════════════════════════════════════════════════════════════════════════
   SCREEN 1 - Editorial split-screen (cinematic lookbook + glass size card)
   ════════════════════════════════════════════════════════════════════════ */
.calc-shell {
  max-width: 1180px;
  margin: 0 auto;
  min-height: 100vh;
  min-height: 100dvh;
  display: grid;
  grid-template-columns: 1.05fr 0.95fr;
  /* the gap is what turns this into two independent, matching glass cards
     instead of one shape fused down the middle - see the redesign note on
     .calc-editorial below. align-items:center (not stretch) lets the
     mascot panel size to its OWN content instead of being force-stretched
     to match the form column's height - stretch was the direct cause of
     the mascot card reading as an oversized, mostly-empty box. */
  gap: 28px;
  align-items: center;
  padding-block: 28px;
  perspective: 1600px;   /* depth field for the 3D card tilt */
}

/* #screen-locked keeps the base display:grid/1.05fr-0.95fr/align-items:center/
   min-height:100dvh above (its single centered thank-you card wants to fill
   the viewport). #screen-calculator no longer needs either: the 2-column
   split now lives inside .calc-card below, so .calc-shell just stacks the
   card and the copyright line beneath it - and forcing it to a full
   100dvh box left a large dead strip below the card+copyright whenever
   their natural content was shorter than the viewport (the "excess space
   below the button" this was one of the causes of). #screen-calculator
   itself gets the same min-height:auto treatment below, for the same
   reason (.screen's base rule also forces 100dvh).

   Mobile (<=920px) is the exception - see the responsive block further
   down, which re-adds min-height:100dvh + vertical centering there once
   the video column is out of the picture (hidden on mobile) and the only
   remaining problem is the form itself sitting flush at the top. */
#screen-calculator .calc-shell { display: block; min-height: auto; }
#screen-calculator { min-height: auto; }

/* ── two-panel layout (#screen-calculator only) ─────────────────────────────
   .calc-card is purely structural here - just the 40/60 grid positioning the
   video and form side by side with a tight 5px gap between them. Each panel
   keeps its own individual card chrome (.calc-editorial and .container both
   still declare their own radius/background/border/shadow/blur below) rather
   than sharing one merged card shell.

   No align-items here on purpose - it's set per-breakpoint below (desktop:
   center; mobile has no second column to align against once .calc-editorial
   is hidden, so its value there is a no-op either way). Neither zone's
   height is ever force-matched to the other's regardless of align-items
   (that would need align-items:stretch, which this never uses) - each card
   is simply as tall as its own content, and the shorter one is positioned
   within the row per the active align-items value (the video's height is
   still bounded via .calc-editorial's max-height cap below, so it can't run
   away regardless). */
#screen-calculator .calc-card {
  display: grid;
  /* 2fr/3fr, NOT 40%/60%: percentage tracks resolve against the container and
     then the gap is ADDED on top, so 40+60+2px was always 2px wider than the
     box holding it. Between 921px (where this two-column layout starts) and
     1180px (where .calc-shell's max-width finally absorbs the excess) that
     overflow reached the viewport and put a horizontal scrollbar on the whole
     page - measured: scrollWidth 1002 at a 1000px viewport, 1122 at 1120px.
     fr tracks divide what is left AFTER the gap, so the same 40:60 proportion
     now always fits exactly. */
  grid-template-columns: 2fr 3fr;
  gap: 2px;
}
@media (min-width: 921px) {
  /* Form is normally the shorter of the two panels - center it vertically
     against the video's full row height instead of pinning it to the top.
     Purely vertical: grid-template-columns above is untouched, so neither
     panel shifts horizontally. */
  #screen-calculator .calc-card { align-items: center; }
  /* The video's source frame is tall/portrait - at 40% of the card's width
     its own aspect-ratio-driven height can run well past what the form's
     real content needs. Capping it here keeps the video card from towering
     over the form card; object-fit:contain on the video (unchanged) means
     it just letterboxes more within a shorter box, never crops. */
  #screen-calculator .calc-card .calc-editorial { max-height: min(700px, 78vh); }
}

/* ── editorial panel ──────────────────────────────────────────────────────
   REDESIGN: this used to be a separate, flush-edge pitch-black box
   (border-radius only on its outer corners) fused directly to the light
   form card next door - two visually unrelated surfaces glued together.
   It's now a soft-dark GLASS card using the exact same radius/shadow/blur
   tokens as #screen-calculator .container (--radius-lg, --shadow-lift,
   --blur-lg), fully rounded and separated by .calc-shell's own gap, so it
   reads as the dark twin of the form card rather than a cutout. ── */
.calc-editorial {
  position: relative;
  display: flex;
  /* column, not row: children stack in REAL document flow so the panel's
     height always accounts for all of them - on tall desktop viewports
     content still settles via margin-top:auto, but on short ones
     (tablet/mobile, where .calc-editorial has no fixed/vh height to lean
     on) they simply stack with no overlap, which position:absolute could
     never guarantee since it ignores box height entirely. */
  flex-direction: column;
  overflow: hidden;
  isolation: isolate;
  border-radius: var(--radius-lg);
  background: linear-gradient(155deg, rgba(10, 11, 12, .94) 0%, rgba(23, 28, 15, .90) 100%);
  border: 1px solid rgba(255, 255, 255, .09);
  box-shadow: var(--shadow-lift);
  -webkit-backdrop-filter: var(--blur-lg);
          backdrop-filter: var(--blur-lg);
}

/* ── LIGHT PANEL (screen-calculator only) ─────────────────────────────────
   The dark card is gone here: this is now a soft ivory/glass surface that
   dissolves into the page canvas so the video side and the form side read
   as two related (but visually distinct) panels rather than one dark card
   glued to a light one.

   SCOPED ON PURPOSE: #screen-locked reuses .calc-editorial *with white
   copy* (.calc-editorial__content/__headline/__meta are all #fff). Making
   the base class light would render that screen white-on-cream, i.e.
   invisible - so the light treatment applies only to this screen and the
   dark base above stays exactly as-is for screen-locked. ── */
#screen-calculator .calc-editorial {
  background:
    linear-gradient(158deg, rgba(255, 252, 244, .92) 0%, rgba(244, 238, 225, .78) 52%, rgba(233, 238, 214, .86) 100%);
  border: 1px solid rgba(255, 255, 255, .75);
  box-shadow: 0 20px 60px rgba(94, 88, 66, .13), inset 0 1px 0 rgba(255, 255, 255, .9);
  /* Flex/grid items default to min-height:auto, which refuses to shrink
     below the content's own intrinsic size even with a max-height set (see
     .calc-card .calc-editorial's max-height cap) - overflow:hidden on this
     element already should neutralize that per spec, but min-height:0 here
     is a belt-and-suspenders guarantee: without it, if the video's
     aspect-ratio-driven intrinsic height ever won that fight, the excess
     would spill past the cap and get clipped by this element's own
     overflow:hidden - genuine cropping, not the harmless letterboxing
     object-fit:contain is supposed to produce instead. */
  min-height: 0;
}
/* soft pear-green ambient bloom instead of the old dark vignette */
#screen-calculator .calc-editorial__aura {
  background:
    radial-gradient(52% 44% at 50% 42%, rgba(141, 182, 0, .20), transparent 72%),
    radial-gradient(58% 48% at 84% 88%, rgba(141, 182, 0, .12), transparent 68%),
    radial-gradient(46% 42% at 10% 96%, rgba(255, 255, 255, .55), transparent 62%);
}
/* dot grid flips to dark-on-light so it stays legible on the cream panel */
#screen-calculator .calc-editorial__grid {
  background-image: radial-gradient(rgba(22, 19, 12, .07) 1px, transparent 1px);
  opacity: .5;
}
#screen-calculator .calc-editorial__scrim {
  background: radial-gradient(120% 70% at 82% 0%, rgba(141, 182, 0, .10) 0%, transparent 58%);
}
/* ambient aura/grid - true background, absolute across the WHOLE panel */
.calc-editorial__aura, .calc-editorial__grid { position: absolute; inset: 0; z-index: 0; }
/* flex:1 fills .calc-editorial's own height - usually a no-op now that
   .calc-card no longer uses align-items:stretch (see above, .calc-editorial
   is no longer force-stretched to match the form column), EXCEPT when the video's
   own aspect-ratio-driven height would exceed .calc-editorial's max-height
   cap (see the #screen-calculator .calc-card .calc-editorial rule above) -
   there, this flex:1 is what makes the visual area actually fill that
   shorter, capped box rather than overflowing it, so the video (still
   object-fit:contain) letterboxes down to fit instead of spilling out.
   min-height:0 pairs with .calc-editorial's own min-height:0 above - same
   "don't let the flex item's own auto min-size beat the capped height and
   spill past overflow:hidden" guarantee, one level deeper in the chain.
   No padding: the video should
   run edge-to-edge inside the panel's own rounded/clipped border. */
.calc-editorial__visual {
  position: relative; z-index: 1; flex: 1;
  min-height: 0;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden;
}
/* object-fit:contain (never cover) - the whole video frame must stay
   visible, no cropping. Anything letterboxed around it shows the panel's
   own cream/glass background (#screen-calculator .calc-editorial above). */
.calc-editorial__video {
  width: 100%; height: 100%;
  object-fit: contain;
  display: block;
}

.calc-editorial__aura {
  position: absolute; inset: -10%;
  background:
    radial-gradient(46% 40% at 50% 38%, rgba(141, 182, 0, .32), transparent 70%),
    radial-gradient(60% 50% at 85% 90%, rgba(141, 182, 0, .14), transparent 65%),
    radial-gradient(50% 45% at 8% 100%, rgba(255, 255, 255, .06), transparent 60%);
  animation: auraDrift 16s var(--ease) infinite alternate;
}
@keyframes auraDrift {
  from { transform: scale(1)     translate(0, 0); }
  to   { transform: scale(1.12) translate(-2%, -3%); }
}
/* faint measurement-grid, faded out toward the edges */
.calc-editorial__grid {
  position: absolute; inset: 0;
  background-image: radial-gradient(rgba(255, 255, 255, .09) 1px, transparent 1px);
  background-size: 26px 26px;
  -webkit-mask-image: radial-gradient(70% 60% at 50% 45%, #000 0%, transparent 75%);
          mask-image: radial-gradient(70% 60% at 50% 45%, #000 0%, transparent 75%);
  opacity: .6;
}

/* subtle pear-green glow accent only - the base panel is dark glass now,
   not a photo, so the old heavy black legibility gradients are gone. */
.calc-editorial__scrim {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: radial-gradient(120% 70% at 82% 0%, rgba(141, 182, 0, .16) 0%, transparent 55%);
}
.calc-editorial__content {
  position: relative; z-index: 2;
  margin-top: auto;
  padding: clamp(28px, 4vw, 56px);
  max-width: 470px; color: #fff;
}
.calc-editorial__kicker {
  display: inline-block;
  font-size: .68rem; font-weight: 700; letter-spacing: .22em; text-transform: uppercase;
  color: #fff;
  background: rgba(255,255,255,.10);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  border: 1px solid rgba(255,255,255,.4);
  padding: 7px 15px; border-radius: var(--pill); margin-bottom: 22px;
}
.calc-editorial__headline {
  font-family: var(--font-ui);
  font-size: clamp(2rem, 3.6vw, 3.05rem); font-weight: 900; line-height: 1.03;
  letter-spacing: -.02em; color: #fff; margin: 0 0 16px;
}
.calc-editorial__lead {
  font-size: .98rem; line-height: 1.6; color: rgba(255,255,255,.82);
  margin: 0 0 22px; max-width: 40ch;
}
.calc-editorial__list { list-style: none; margin: 0 0 26px; padding: 0; display: grid; gap: 11px; }
.calc-editorial__list li {
  position: relative; padding-inline-start: 22px;
  font-size: .9rem; font-weight: 500; color: rgba(255,255,255,.92);
}
.calc-editorial__list li::before {
  content: ""; position: absolute; inset-inline-start: 0; top: .5em;
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--pear); box-shadow: 0 0 12px var(--pear-glow);
}
.calc-editorial__meta {
  display: block; font-size: .68rem; letter-spacing: .18em; text-transform: uppercase;
  color: rgba(255,255,255,.55);
  border-top: 1px solid rgba(255,255,255,.18); padding-top: 16px;
}

/* ── calculator column ───────────────────────────────────────────────────── */
.calc-main {
  display: flex; flex-direction: column; justify-content: center;
  padding: 20px clamp(16px, 3vw, 40px) 40px;
}
/* #screen-calculator only: drop the base's top padding - the extra 20px
   .calc-main would otherwise add is just dead space above the form,
   regardless of how .calc-card aligns it within the row (desktop centers
   it vertically now, see .calc-card's align-items above; on mobile it's
   the row's only item either way). #screen-locked keeps the base
   justify-content:center above - its single centered thank-you card has no
   video counterpart to align against.
   padding-bottom trimmed too - .container brings its own bottom padding now
   (see above), so the base 40px here was stacking on top of that as a
   second layer of bottom spacing after the button, part of the same
   leftover-space audit as the .container changes above. */
#screen-calculator .calc-main { padding-top: 0; padding-bottom: 16px; }
.calc-main .step-progress { width: 100%; max-width: 472px; padding-top: 12px; }
#screen-calculator .container { margin: 0 auto 12px; }
.calc-main__copyright {
  margin: 16px auto 0; text-align: center;
  font-size: .68rem; letter-spacing: .06em; color: var(--muted-2);
}

/* ── responsive ───────────────────────────────────────────────────────────── */
@media (max-width: 920px) {
  .calc-shell { grid-template-columns: 1fr; min-height: auto; }
  .calc-editorial {
    min-height: 0;   /* screen-locked's copy still sizes its own panel naturally */
    border-radius: var(--radius-lg);   /* fully rounded now on every width - no more RTL-specific split to keep in sync */
  }
  .calc-editorial__content { padding: 20px clamp(18px, 5vw, 30px) 24px; }
  .calc-editorial__headline { font-size: clamp(1.7rem, 7vw, 2.3rem); }
  .calc-editorial__lead { margin-bottom: 6px; }
  .calc-editorial__list { display: none; }
  .calc-main { padding-top: 6px; }

  /* Video panel is desktop-only - hidden outright on mobile (not shrunk to
     a strip) rather than fighting its portrait aspect ratio into a tiny,
     misaligned sliver. Mobile shows just the form column at full width,
     same as before the video existed. #screen-locked has no video markup
     in the first place, so nothing to scope away from there. */
  #screen-calculator .calc-editorial { display: none; }
  /* With .calc-editorial gone, .calc-card would otherwise still reserve its
     40% column - .calc-main needs the collapse to 1fr to actually take the
     freed-up width, same as .calc-shell did for the old 2-column layout. */
  #screen-calculator .calc-card { grid-template-columns: 1fr; }

  /* Vertically center the form on the mobile screen instead of it sitting
     flush at the top with a large dead gap below (a side effect of
     #screen-calculator's own min-height:auto above, which is a desktop-only
     fix for a *different* dead-space problem - the merged/tall video column
     no longer exists here since .calc-editorial is hidden, so there's
     nothing left needing that auto-height behavior at this breakpoint).
     Scoped to .active ON PURPOSE: screen visibility is class-driven
     (.screen { display:none } / .screen.active { display:block }), and a bare
     #screen-calculator { display:flex } outranks .screen's display:none by ID
     specificity - which kept Screen 1 painted on mobile even after the
     Screen 1 → Screen 2 swap removed .active, so the fitting room sat a full
     viewport below a still-visible size form ("stuck on size selection").
     With .active in the selector this only applies while the screen is
     actually shown, and the base .screen hide works again the moment it isn't.
     justify-content:center centers .calc-shell (the sole child) vertically;
     padding-bottom > padding-top shrinks the centering box from the bottom
     only, which pulls the midpoint - and so the centered card - up from
     dead-center by half the difference (here, 4vh above true center). */
  #screen-calculator.active {
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding-top: 0;
    padding-bottom: 8vh;
  }
}
@media (max-width: 480px) {
  .calc-editorial__meta { display: none; }
  .calc-shell { gap: 18px; padding-block: 18px; }
  .calc-editorial__content {
    width: 100%;
    text-align: center;
    padding: 18px 22px 24px;
  }
  .calc-editorial__headline { margin-bottom: 12px; }
  .calc-editorial__lead { max-width: 30ch; margin-inline: auto; }
}

/* ════════════════════════════════════════════════════════════════════════
   SCREEN 2 - fitting room
   ════════════════════════════════════════════════════════════════════════ */

/* product-isolation status bar - frosted white glass, pear-green cues */
.focus-bar {
  /* STATIC, not sticky: this bar and .app-header used to both claim top:0 (the
     header only on phones, ≤768px - see the .focus-bar:not([hidden]) ~
     .app-header rule this replaces), which the header ceded to this bar
     entirely rather than let two sticky elements overlap at the same offset.
     That meant the PEAR logo / language / edit controls scrolled away the
     moment an item's focus status appeared, leaving only the "measuring: X"
     strip pinned - reported as the main header vanishing while a sub-bar
     stayed stuck, with its own content overlapping whatever scrolled up
     beneath it. The header is the primary chrome; this is a subordinate
     status strip, so it should give way to the header, not the other way
     round. As ordinary in-flow content it now simply scrolls up and passes
     BEHIND the still-sticky header (which sits at z-index:40, well above
     anything ordinary in-flow content stacks at, so its opaque glass panel
     visually covers this bar as it goes under) instead of racing it for the
     same pinned slot. z-index/top are dropped below, not just left at their
     old values - both are inert on position:static, so keeping them would
     just be dead, misleading declarations. */
  position: static;
  display: flex; align-items: center; gap: 12px;
  /* Clear of the header row above it. .app-header has padding-bottom:0 at
     desktop widths, so this bar's own top padding was the ONLY thing between
     the "measuring: X" line and the header controls - and its glass panel
     starts at that boundary, not at the text, which is what read as the two
     colliding. A margin (not more padding) so the gap is OUTSIDE the panel:
     it separates the two surfaces instead of just insetting this one's text. */
  margin-top: 10px;
  padding: 22px clamp(16px,5vw,36px);   /* was 15px - sat too close to the viewport's top edge */
  background: linear-gradient(135deg, rgba(255,255,255,.6), rgba(255,255,255,.4));
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--ink);
  border-bottom: 1px solid var(--glass-border);
  box-shadow: var(--shadow-soft);
  font-size: .95rem; font-weight: 600;
  /* On the BASE rule (not just the mobile/is-scrolled overrides) so scrolling
     back UP also animates at every width - previously only the ≤768px block
     and .is-scrolled itself declared this, so desktop widths snapped instantly
     the moment .is-scrolled was removed instead of tweening back. This is the
     RETRACTING direction (is-scrolled → base): a smooth, no-overshoot ease,
     since "settling back into place" should read as calm, not springy. */
  transition: padding .45s var(--ease);
}
.focus-bar[hidden] { display: none; }
.focus-bar__dot {
  width: 9px; height: 9px; border-radius: 50%; background: var(--pear);
  box-shadow: 0 0 0 0 var(--pear-glow); animation: pulseDot 1.8s infinite; flex-shrink: 0;
  transform: scale(1);
}
@keyframes pulseDot {
  0% { box-shadow: 0 0 0 0 rgba(141,182,0,.5); }
  70% { box-shadow: 0 0 0 10px rgba(141,182,0,0); }
  100% { box-shadow: 0 0 0 0 rgba(141,182,0,0); }
}
/* Arrival flourish - a quick overshoot pop the instant the dot lands in its
   flush position, layered on top of (not replacing) the constant ambient
   pulse above; the two animate different properties so they never fight. */
@keyframes dotPop {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.5); }
  100% { transform: scale(1); }
}
.focus-bar.is-scrolled .focus-bar__dot {
  animation: pulseDot 1.8s infinite, dotPop .5s var(--spring);
}
.focus-bar__text strong { font-family: var(--font-ui); font-weight: 800; }
.focus-bar__badge {
  margin-inline-start: auto; font-size: .76rem; font-weight: 800; color: var(--pear-deep);
  background: var(--pear-tint); border: 1px solid rgba(141,182,0,.4);
  padding: 5px 13px; border-radius: var(--pill); white-space: nowrap;
}
.focus-bar__badge:empty { display: none; }

.app-header {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  /* padding-top from the shared token, so the row and the two absolute corner
     controls resolve their top edge from ONE number - see --hdr-pad-t. */
  padding: var(--hdr-pad-t) clamp(16px,5vw,36px) 0;
  max-width: 760px; margin: 0 auto;
  box-sizing: border-box;

  /* ── Corner reservation (see TOP-CORNER CONTROL GEOMETRY) ──────────────
     The fixed .lang-switch / .profile-btn are pinned to the VIEWPORT edges,
     while this box is capped at 760px and centred - so the space it already
     has between its own edge and the viewport edge counts toward clearing
     them. That inset is (100vw - box width) / 2, and only the SHORTFALL
     needs to become padding.
     max() means this is a strict no-op wherever the centred box is already
     wide of the corners (~1008px up, i.e. every ordinary desktop width) -
     the design's own clamp() padding wins there and the perfected desktop
     header is untouched. It engages only in the band where the two really
     do collide, which measurement put at ≤907px: at 800px the cart button
     sat 42×34px underneath the language switch and was completely
     unclickable, at a perfectly ordinary laptop/tablet width. */
  --hdr-pad-x: clamp(16px,5vw,36px);   /* the ordinary edge padding, with no corner control to clear */
  padding-left:  max(var(--hdr-pad-x), calc(var(--hdr-gutter-l) - (100vw - min(760px, 100vw)) / 2));
  padding-right: max(var(--hdr-pad-x), calc(var(--hdr-gutter-r) - (100vw - min(760px, 100vw)) / 2));
}

/* Both corner controls scroll away with the page (see .lang-switch /
   .profile-btn), so the moment they have left the viewport the gutters
   reserved to clear them are dead space - an ~86px hole at the head of the row
   and ~52px at its tail on a phone, which reads as a layout bug rather than a
   deliberate margin. Reclaim both and let the row re-centre. .app-header
   already transitions `padding`, so this glides in on the same easing as the
   existing scroll compaction rather than snapping.
   body.corners-cleared is set by mobile-ux.js only once BOTH controls are
   ENTIRELY past the top edge - deliberately NOT the shared .is-scrolled flag,
   which fires at 12px while they are still half on screen and would slide the
   header's contents underneath them.
   Scoped to ≤768px because that is exactly where .app-header is sticky. Above
   it the header is an ordinary block that scrolls away WITH the corner
   controls, so there is no surviving gutter to reclaim - and collapsing it
   there is actively wrong: the desktop header is a centred 760px box whose
   own margin does part of the clearing, so dropping its padding pulls the row
   back into the language switch's column (measured: a 39×29px overlap at
   820-900px). */
@media (max-width: 768px) {
  body.corners-cleared .app-header {
    padding-left:  calc(var(--safe-l) + var(--hdr-pad-x));
    padding-right: calc(var(--safe-r) + var(--hdr-pad-x));
  }
}
/* ── Fullscreen segmented switch (was "Back to Store", a plain link/pill) ──
   Apple Liquid Glass segmented control: a translucent sliding thumb behind
   two icon segments (Normal / Fullscreen), matching the same segmented-switch
   glass language now shared with .lang-switch (both built on this same
   sliding-thumb architecture). Supports a plain click/tap on either half AND
   press-and-drag the thumb across the track (setupFullscreenToggle() in
   app.js drives both through the same pointerdown/move/up handlers).
   direction:ltr locks this control's OWN internal geometry to a fixed
   physical layout regardless of the page's dir - "Normal" is always slot 0
   or "left" internally, "Fullscreen" is always slot 1 - so the drag/
   translateX math in app.js never needs an RTL branch. This only overrides
   direction INSIDE the button; its position within the RTL nav row (physical
   right, where "Back to Store" used to sit) is entirely unaffected. */
.fs-switch {
  align-self: center;
  position: relative; direction: ltr;
  display: inline-flex; align-items: center;
  /* Shared header-control height. The track was 36px (26px segment + 2x4px
     track padding + its 1px specular border each side); the segments keep
     their own size and centre inside it, and __thumb already sizes itself as
     the track height minus that padding, so it follows this automatically. */
  height: var(--hdr-control-h);
  padding: var(--switch-pad); margin: 0; border: 0;
  appearance: none; -webkit-appearance: none;
  border-radius: var(--pill);
  background: var(--glass-soft);
  -webkit-backdrop-filter: blur(16px) saturate(180%); backdrop-filter: blur(16px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, .4);   /* specular border, per the Liquid Glass spec */
  box-shadow: var(--shadow-soft);
  cursor: pointer; touch-action: none;   /* pointer drag owns touch gestures here, not page scroll */
}
.fs-switch:hover { box-shadow: 0 8px 22px rgba(0, 0, 0, .12); }
.fs-switch:active { cursor: grabbing; }
.fs-switch:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--pear-ring), var(--shadow-soft); }

/* The translucent pill indicator - slides between slot 0/1 via --thumb-x
   (set inline by app.js, live during drag; snapped to the exact segment
   width on release/click). Ambient soft drop-shadow + specular border, both
   per the Liquid Glass spec. */
.fs-switch__thumb {
  position: absolute; top: var(--switch-pad); left: var(--switch-pad); z-index: 0;
  /* Width is authored to EQUAL one segment - app.js measures this node to get
     the live segment width, so the drag/snap math follows any responsive
     resize automatically instead of trusting a hardcoded constant. */
  width: var(--fs-seg-w); height: calc(100% - 2 * var(--switch-pad));
  border-radius: var(--pill);
  background: rgba(255, 255, 255, .82);
  border: 1px solid rgba(255, 255, 255, .4);
  box-shadow: 0 3px 10px rgba(0, 0, 0, .18), inset 0 1px 0 rgba(255, 255, 255, .6);
  transform: translateX(var(--thumb-x, 0px));
  transition: transform .5s var(--spring);   /* the release "snap" */
  pointer-events: none;   /* the track (.fs-switch itself) owns all pointer handling */
}
/* live-drag: no transition lag while the thumb is tracking the pointer 1:1 -
   the spring above only plays on the SNAP once the pointer is released. */
.fs-switch.is-dragging .fs-switch__thumb { transition: none; }

.fs-switch__seg {
  position: relative; z-index: 1;   /* above the absolutely-positioned thumb */
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--fs-seg-w); height: 26px;
  color: var(--muted);
  transition: color .3s var(--ease);
}
.fs-switch__seg.is-active { color: var(--ink); }
.fs-switch__icon { width: 15px; height: 15px; flex: none; }

@media (prefers-reduced-motion: reduce) {
  .fs-switch__thumb { transition: none; }
}

/* Edit Measurements - same frosted pill language as the old Back-to-Store
   link, but pear-green accented (an action, not a navigation link). */
.btn-edit-measurements {
  align-self: center;
  display: inline-flex; align-items: center; gap: 6px;
  /* THE OUTLIER: this was ~27px (12.8px of line-height:1 text + 7px padding
     each side) sitting next to a 42px cart button - the single widest gap in
     the row. Height is declared now rather than left to fall out of the type
     size, and the vertical padding goes with it; the horizontal padding stays,
     because that is what gives the pill its shape. */
  height: var(--hdr-control-h);
  padding: 0 13px; margin: 0; border: 0;
  border-radius: var(--pill);
  background: var(--pear-tint);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  border: 1px solid rgba(141, 182, 0, .35);
  /* LEGIBILITY FIX: var(--pear-deep) (#5f7d00) on var(--pear-tint) over the page's
     cream backdrop computes to only ~4.08:1 contrast - under the 4.5:1 WCAG AA
     minimum for text this size/weight. #4a6300 on the same background is ~5.9:1.
     Scoped to just this button rather than changing --pear-deep globally (that
     variable is the app's one shared accent color, used well beyond the header). */
  color: #4a6300;
  font-family: "Inter", sans-serif; font-size: .8rem; font-weight: 700;
  white-space: nowrap; cursor: pointer;
  transition: transform .3s var(--ease), box-shadow .3s var(--ease), border-color .3s var(--ease), background .3s var(--ease);
}
.btn-edit-measurements:hover {
  transform: translateY(-1px);
  background: rgba(141, 182, 0, .24); border-color: rgba(141, 182, 0, .55);
  box-shadow: 0 8px 22px rgba(141, 182, 0, .2);
}
.btn-edit-measurements:active { transform: scale(.96); }
.btn-edit-measurements svg  { flex: none; }   /* icon is fixed-size - only the label may shrink */
/* BUG FIX: was overflow:hidden;text-overflow:ellipsis, which truncated the
   label to "עריכת..." well before the header actually ran out of room -
   .app-header__nav > * { flex-shrink:0 } above now guarantees this button is
   never squeezed below its natural content width in the first place (the
   brand lockup gives way instead), so the label just always renders in full;
   ellipsis is no longer needed as a safety net here. */
.btn-edit-measurements span { line-height: 1; white-space: nowrap; }
/* NOTE: the old ≤480px rule that hid these buttons' text (icon-only fallback)
   was removed - the 2-row mobile header (see the "SMALL-PHONE HEADER" block
   further down) gives both buttons their own row/column, so the full label
   ("חזרה לחנות" / "עריכת מידות") always fits and stays visible instead of
   collapsing to an icon. */

/* LAYOUT FIX: same shrink-and-ellipsize treatment as the nav pills above, so the
   "PEAR" wordmark + subtitle truncate gracefully instead of overflowing the brand
   zone into the actions zone (.app-header__titles already carries min-width:0). */
.app-header__brand {
  font-family: var(--font-ui); font-weight: 900; font-size: 1.4rem; letter-spacing: .04em;
  line-height: 1; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  background: linear-gradient(120deg, var(--pear-deep), var(--pear));
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
.app-header__sub {
  color: var(--muted); font-size: .84rem; line-height: 1.2; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Removed everywhere, not just mobile - a bare grey dot with no label reads
   as a stray UI fragment next to the cart button (it already had a comment
   below calling it purely decorative and hiding it at <=768px; this just
   drops the width condition). setConn() (app.js) still updates it defensively
   if a #engineBadge element exists, but no-ops harmlessly since it no longer
   renders. */
.app-header__engine { display: none; }

.fitting-container { max-width: 760px; }
.personal-title { font-size: 1.6rem; font-weight: 800; margin: 8px 0 26px; }
.personal-title span {
  background: linear-gradient(120deg, var(--pear-deep), var(--pear));
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}

/* active garment chip - floating glass tile */
.active-garment {
  display: flex; align-items: center; gap: 16px;
  padding: 14px; margin-bottom: 22px;
  border: 1px solid var(--glass-border); border-radius: var(--radius);
  background: var(--glass);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  box-shadow: var(--shadow-soft);
  transition: transform .4s var(--ease), box-shadow .4s var(--ease);
}
.active-garment:hover { transform: translateY(-3px); box-shadow: var(--shadow); }
.active-garment[hidden] { display: none; }
/* Positioning context for .ag-prep, which covers the whole tile rather than just the
   thumbnail - see the asset-preparation block further down. */
.active-garment { position: relative; }
.active-garment__media {
  width: 60px; height: 60px; border-radius: 14px; overflow: hidden;
  border: 1px solid var(--glass-border); background: rgba(255,255,255,.4); flex-shrink: 0;
}
.active-garment__eyebrow { display: block; font-size: .7rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--pear-deep); }
.active-garment__name { display: block; font-family: var(--font-ui); font-size: 1.05rem; font-weight: 800; margin: 2px 0; color: var(--ink); }
.active-garment__type { font-size: .82rem; color: var(--muted); }

/* ── COMBINED (FRONT|BACK) composite thumbnail ─────────────────────────────
   The chip's default media box is a 60px square whose <img> is object-fit:cover
   (see the shared product-media rule further down). A composite is roughly 2:1,
   so cover would crop it to a slice of the FRONT panel and hide the back entirely
   - exactly what this thumbnail exists to show. Widen the box to the composite's
   own proportions and switch to `contain` so both panels stay fully visible.
   `contain` letterboxes rather than crops, hence the neutral backing colour. */
.active-garment.is-composite .active-garment__media {
  /* aspect-ratio is the actual sizing mechanism (2:1 ≈ createGarmentComposite's real
     output - two similarly-shaped garment photos side by side); the width is a plain
     fallback for the ~4% of browsers without aspect-ratio support, computed off the
     SAME ratio against the row's fixed 60px height so the two never disagree. */
  width: 120px;
  aspect-ratio: 2 / 1;
  background: var(--bg);         /* flat light gray - reads clearly under `contain`, unlike a translucent overlay */
}
.active-garment.is-composite .active-garment__media img {
  object-fit: contain;           /* show BOTH panels, never crop to one */
  object-position: center;       /* override the default `center top` framing */
  padding: 2px;                  /* keeps the outer panel edges off the rounded corners */
}
/* The chip lifts on hover and the shared rule scales product images; a contained
   composite has visible letterbox margins, so that zoom reads as a wobble. */
.active-garment.is-composite:hover .active-garment__media img { transform: none; }

@media (max-width: 380px) {
  /* Narrow phones: the name/type column needs the room more than the thumbnail does.
     aspect-ratio is unchanged, so this only affects the fallback width. */
  .active-garment.is-composite .active-garment__media { width: 96px; }
}

/* ── "finding/generating a back view" indicator ────────────────────────────
   A fresh synthesis measured ~27s in production (Gemini image generation is
   genuinely slow) - long enough that a bare, static front photo with no signal
   that anything is happening reads as broken, not as loading. This overlays a
   spinner on the (still-visible, still front-photo) thumbnail for that window.
   thumbIsPending() in app.js guarantees this and .is-composite are mutually
   exclusive, so there is no state where both rules could visually collide. */
.active-garment.is-pending .active-garment__media {
  position: relative;
}
.active-garment.is-pending .active-garment__media::after {
  content: "";
  position: absolute; inset: 0;
  background: rgba(16, 16, 18, .38);
  display: flex;   /* the spinner itself is drawn by ::before below, layered via z-index */
}
.active-garment.is-pending .active-garment__media::before {
  content: "";
  position: absolute; top: 50%; left: 50%; z-index: 1;
  width: 18px; height: 18px; margin: -9px 0 0 -9px;
  border-radius: 50%;
  border: 2.5px solid rgba(255, 255, 255, .35);
  border-top-color: #fff;
  animation: pear-pending-spin .8s linear infinite;
}
@keyframes pear-pending-spin { to { transform: rotate(360deg); } }
/* Respect reduced-motion: keep the dimmed overlay (still communicates "in
   progress") but drop the spin so the indicator doesn't fight that preference. */
@media (prefers-reduced-motion: reduce) {
  .active-garment.is-pending .active-garment__media::before { animation: none; }
}

/* ══ Liquid Glass asset-preparation overlay ═══════════════════════════════════
   THE BUG THIS CLOSES: the chip used to paint the UNVALIDATED handover photo at full
   opacity behind a small spinner (.is-pending above) for the entire 2.5s-27s the
   front/back verdict took to arrive, then visibly mutate when the correction landed.
   Showing an asset we already know we do not trust is the problem; dimming it was
   never going to be the fix. This replaces it outright for that window: the garment
   media is hidden, a glass progress panel takes the tile, and the finished card is
   revealed in ONE step once the real asset is bound.

   .is-pending IS NOT RETIRED. It still owns the case this does not cover - a
   background composite build on a chip whose photo is already validated - and the two
   are mutually exclusive in practice because assetPrepActive() and thumbIsPending()
   are driven from the same livePendingReason() predicate. Kept as-is deliberately.

   VISIBILITY, NOT DISPLAY, for the hidden children: the tile must not resize or reflow
   when the overlay lifts, or the "single-step transition" becomes a layout jump. The
   glass panel therefore inherits the box the real content already reserved. */
.active-garment.is-preparing > .active-garment__media,
.active-garment.is-preparing > .active-garment__meta {
  visibility: hidden;
}
/* The lift would read as the panel wobbling while a number climbs inside it. */
.active-garment.is-preparing:hover { transform: none; box-shadow: var(--shadow-soft); }

.ag-prep {
  position: absolute; inset: 0; z-index: 2;
  display: flex; align-items: center; gap: 14px;
  padding: 0 16px;
  border-radius: inherit;
  overflow: hidden;                       /* clips the sheen sweep to the rounded tile */
  border: 1px solid rgba(255, 255, 255, .3);
  background: rgba(255, 255, 255, .25);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
          backdrop-filter: blur(20px) saturate(180%);
  /* Specular pass: a bright top edge and a dimmer bottom one read as a lens with
     thickness rather than a flat translucent rectangle, over a soft ambient drop. */
  box-shadow:
    inset 0  1px 0 rgba(255, 255, 255, .75),
    inset 0 -1px 0 rgba(255, 255, 255, .22),
    0 8px 32px rgba(0, 0, 0, .10),
    0 1px  2px rgba(0, 0, 0, .04);
  animation: ag-prep-in .28s var(--ease) both;
}
.ag-prep[hidden] { display: none; }

/* Slow specular sweep. Purely decorative, hence aria-hidden in the markup. */
.ag-prep__sheen {
  position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(105deg,
    transparent 30%, rgba(255, 255, 255, .55) 48%, transparent 66%);
  transform: translateX(-100%);
  animation: ag-prep-sheen 2.6s var(--ease) infinite;
}

.ag-prep__readout { flex-shrink: 0; }
.ag-prep__pct {
  font-family: var(--font-ui);
  font-size: 1.5rem; font-weight: 800; line-height: 1;
  color: var(--ink);
  /* Tabular figures - without these the counter's width changes on nearly every
     tick and the phase text beside it jitters for the whole run. */
  font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1;
}
.ag-prep__sign { font-size: .78rem; font-weight: 700; font-style: normal; color: var(--muted); margin-left: 1px; }

.ag-prep__copy { display: flex; flex-direction: column; gap: 7px; min-width: 0; flex: 1; }
.ag-prep__phase {
  font-size: .76rem; font-weight: 600; letter-spacing: .01em;
  color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ag-prep__track {
  height: 3px; border-radius: 3px; overflow: hidden;
  background: rgba(10, 10, 11, .10);
}
.ag-prep__fill {
  display: block; height: 100%; width: 0%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--pear-deep), var(--pear));
  /* Eased in JS frame by frame, so this only has to smooth over the gap between
     coarse milestone jumps - a long transition here would fight that easing. */
  transition: width .18s linear;
}

@keyframes ag-prep-in   { from { opacity: 0; } to { opacity: 1; } }
@keyframes ag-prep-sheen { 0% { transform: translateX(-100%); } 55%, 100% { transform: translateX(100%); } }

/* The panel still communicates progress through the number and the bar, so the
   motion is the only thing dropped here - not the overlay itself. */
@media (prefers-reduced-motion: reduce) {
  .ag-prep { animation: none; }
  .ag-prep__sheen { animation: none; opacity: 0; }
  .ag-prep__fill { transition: none; }
}

@media (max-width: 380px) {
  .ag-prep { gap: 10px; padding: 0 12px; }
  .ag-prep__pct { font-size: 1.25rem; }
  .ag-prep__phase { font-size: .7rem; }
}

/* Full-look duo chip */
.active-garment.is-duo .active-garment__media { width: 120px; }
.active-garment__media > span.ag-duo {
  display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
}
.ag-duo > span { border: 1px solid var(--glass-border); border-radius: 9px; overflow: hidden; }

/* ── camera card - premium rounded glass-framed stage ──────────────────── */
.camera-card {
  position: relative; width: 100%; max-width: 470px; margin: 0 auto;
  /* Responsive stage - no rigid 3/4 lock. A gentle portrait default (less
     zoom/crop than 3/4) that never outgrows the viewport; the desktop rule
     further down widens it so landscape webcams show with minimal crop. */
  aspect-ratio: 4 / 5; max-height: min(78vh, 640px); border-radius: 24px;
  overflow: hidden; background: #050505;
  border: 1px solid var(--glass-border);
  box-shadow: var(--shadow), inset 0 0 0 1px rgba(255,255,255,.06);
  transition: box-shadow .6s var(--ease), transform .5s var(--ease);
}
/* thin refracting inner frame */
.camera-card::after {
  content: ""; position: absolute; inset: 0; z-index: 5; pointer-events: none;
  border-radius: inherit; border: 1px solid rgba(255,255,255,.14);
}
.camera-card #webcam,
.camera-card #aiVideo,
.camera-card #resultCanvas {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; transform: scaleX(-1);
  background: #050505;
}
.camera-card #aiVideo { display: none; }
.camera-card #resultCanvas { display: none; transform: none; }
.camera-card.show-result #resultCanvas { display: block; }
.camera-card.show-result #webcam { visibility: hidden; }

/* LIVE try-on: AI-edited stream shown directly.
   ── scaleX(-1), AND IT USED TO BE transform:none ────────────────────────────
   The selfie flip now lives HERE instead of on the outgoing WebRTC canvas. It used to be
   applied in drawFrame() before frames left the browser, which meant Decart was
   conditioned on a mirrored world and rendered the garment's chest text backwards to
   match the scene ("PEAK" -> "916tim9"); see the long note on drawFrame in app.js.
   Moving it to the display layer is visually identical for the shopper - one flip either
   way - but the model now sees reality. #aiVideo's DECODED frames are therefore
   un-mirrored, which is what every canvas that samples it now assumes. */
/* ── THE LIVE MIRROR, AND THE ONE PLACE IT IS DECIDED ─────────────────────────
   translateZ(0) is composed in HERE rather than assigned inline from onRemoteStream.
   It used to be `aiVideo.style.transform = "translateZ(0)"`, which - being inline -
   outranked this rule and silently owned the transform, so the flip below never
   applied and the feed rendered with no mirror at all. Both intents now live in one
   declaration where they cannot race: the flip is the selfie view, translateZ(0) is
   the GPU-compositing hint, and `transform` on #aiVideo is written nowhere else.

   THE TRADE THIS RULE SETTLES - read before changing it. The garment and the body
   are the same pixels, so the flip count cannot differ between them:
     scaleX(-1) here  -> motion feels natural (selfie), garment text reads MIRRORED
     no flip here     -> garment text reads correctly, motion feels reversed
   Decart renders the garment's text to match the orientation of the scene it is fed,
   which is why both of those have been reported as bugs from opposite directions.
   There is no arrangement that gives both; see MIRROR_POLICY in app.js. */
.camera-card.show-live #aiVideo { display: block; transform: scaleX(-1) translateZ(0); }
.camera-card.show-live #webcam { visibility: hidden; }

/* ── Lower-body compositing guard (config.js LOWER_BODY_GUARD_ENABLED) ────────
   Stacked ABOVE #aiVideo in DOM order (index.html), so it paints on top by default -
   no z-index needed to win against the video layers, only against the frame border
   (::after, z-index 5) and swatch rail (z-index 7), which must both stay above it.
   transform:none, unlike the base #webcam/#aiVideo rule: startLowerBodyGuard() reads
   raw pixels FROM #webcam via drawImage() (which ignores CSS transforms - it sees the
   decoded frame, not the mirrored display) and applies its OWN ctx.scale(-1,1) before
   drawing, so the flip is already IN the canvas and a CSS transform here would stack a
   second one on top of it.

   UNCHANGED BY THE UN-MIRRORING REFACTOR, and worth stating because it looks like it
   should have moved with everything else. The old wording claimed this matched
   "#aiVideo's un-mirrored orientation", which stopped being true when the selfie flip
   moved from the outgoing WebRTC canvas to the display layer (see drawFrame in app.js):
   #aiVideo's decoded frames are now un-mirrored and its DISPLAY carries scaleX(-1).
   This guard still lines up, because it samples #webcam - which was never flipped at
   source and is therefore unaffected - and bakes its own flip, landing on the same
   on-screen orientation as #aiVideo by a different route. Both are selfie on screen;
   only the layer that applies the flip differs. */
/* ── Body-presence prompt ─────────────────────────────────────────────────────
   Shown while the presence gate waits for the shopper to step into frame. Visibility is
   owned by app.js alone - showPresenceOverlay() / hidePresenceOverlay(). The only class
   here, .is-leaving, is the exit fade those two drive; it is not a second gate. `hidden`
   (display:none) is still the resting state, and it is also what stops every keyframe
   below from ticking at all once the shopper is confirmed. */
#presenceOverlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  padding-inline: 16px;
  z-index: 6;                 /* above the video, below the scan overlay and any modal */
  /* Never intercepts clicks: the shopper must still be able to hit Stop, change a
     swatch, or press go-live again while this is up. */
  pointer-events: none;
  background: rgba(12, 14, 12, 0.42);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  animation: presenceFadeIn 220ms ease-out both;
}
#presenceOverlay[hidden] { display: none; }
/* The gate confirmed. The fade starts in the same tick gate.feed() returned true;
   `hidden` lands PRESENCE_OVERLAY_FADE_MS (app.js) later. Keep the two durations equal. */
#presenceOverlay.is-leaving { animation: presenceFadeOut 180ms cubic-bezier(0.4, 0, 0.2, 1) both; }

/* ── Step-back guide ──────────────────────────────────────────────────────────
   Replaces a static dashed pill that said "stand here" without saying how. The gate's
   commonest miss is a shopper who is too CLOSE - hips and knees cropped off the bottom -
   so the loop acts the fix out. One 3.5s cycle, markup in index.html:

     0-40%   STEP BACK   the figure starts too big for the frame (1.35, ghosted to 0.7,
                         legs spilling out of the bottom) and walks back into it (0.85),
                         bobbing once per step
     40-55%  LOCK        the dashed frame turns mint and pulses: full body in frame
     55-75%  DRESS       a tee, its sleeves, then shorts draw on (stroke-dashoffset)
     75-90%  CHECK FIT   the arms open 15deg outward
     90-100% RESET       the figure fades; the jump back to 1.35 happens while invisible

   WHAT RUNS WHERE. The step-back, bob, lock pulse, arm swing and every fade are transform
   and opacity on HTML wrappers - compositor-only, no main-thread work per frame. That is
   why the moving parts are separate <div>s rather than <g>s: a CSS transform on an SVG
   child is main-thread paint in most engines. Two things are paint by nature and are
   kept deliberately small: the marching dash on the frame and the garment line draw
   (only moving during 55-75%). Both repaint a layer no bigger than this ~170x260 box.

   NO backdrop-filter ON THE PILL. #presenceOverlay already has one and runs an opacity
   animation, which makes it the backdrop root for everything inside it: a nested blur
   would sample the overlay's own tint, never the camera, and cost a GPU pass for nothing.
   The "glass" is gradient + inner highlight over the overlay's existing blur.

   Stroke widths are in viewBox units, and the box renders at ~1 unit per CSS pixel at its
   250px cap, so 3px is 3px at rest and scales with the figure as it steps back. */
.presence-guide {
  position: relative;
  z-index: 0;                     /* its own stack, so the caption can sit above a figure
                                     that overflows the frame at the start of a loop */
  flex: none;
  height: min(250px, 56%);
  aspect-ratio: 170 / 260;
  max-width: 100%;
}
.presence-guide > *,
.pg-figure > *,
.pg-zoom > *,
.pg-walk > *,
.pg-arm > * {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.presence-guide svg { display: block; overflow: visible; }

.pg-glass {
  border-radius: 999px;
  background: linear-gradient(165deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.05) 48%, rgba(255, 255, 255, 0.02) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.24),
    inset 0 -24px 40px rgba(0, 0, 0, 0.14),
    0 14px 40px rgba(0, 0, 0, 0.28);
}

/* The success glow is a STATIC box-shadow whose layer only changes opacity/scale, which
   is the composited way to "animate a glow" - animating box-shadow or drop-shadow itself
   would re-rasterise a blur every frame. ::after is the one-shot expanding lock ring. */
.pg-glow {
  border-radius: 999px;
  opacity: 0;
  box-shadow:
    0 0 0 1.5px rgba(98, 243, 207, 0.85),
    0 0 26px 3px rgba(98, 243, 207, 0.42),
    inset 0 0 24px rgba(98, 243, 207, 0.2);
  will-change: transform, opacity;
  animation: pgLockGlow 3.5s linear infinite;
}
.pg-glow::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 2px solid rgba(98, 243, 207, 0.9);
  opacity: 0;
  will-change: transform, opacity;
  animation: pgLockRing 3.5s linear infinite;
}

.pg-frame rect {
  fill: none;
  stroke: rgba(255, 255, 255, 0.78);
  stroke-width: 2px;
  stroke-dasharray: 6 6;
  animation: pgFrame 3.5s linear infinite;
}

.pg-figure {
  opacity: 0;
  will-change: opacity;
  animation: pgFigure 3.5s linear infinite;
}
.pg-zoom {
  /* 53% puts the fitted (0.85) figure dead centre in the pill */
  transform-origin: 50% 53%;
  will-change: transform, opacity;
  animation: pgStepBack 3.5s linear infinite;
}
.pg-walk {
  will-change: transform, opacity;
  animation: pgWalk 3.5s linear infinite;
}

.pg-lines {
  will-change: opacity;
  animation: pgLinesDim 3.5s linear infinite;
}
.pg-lines circle,
.pg-lines path,
.pg-lines rect {
  fill: transparent;
  stroke: #ffffff;
  stroke-width: 3px;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* pathLength=100 on every garment path, dash 100 / gap 110: offset 105 sits the whole
   path inside the gap (nothing drawn - not even a round-cap dot), offset 0 draws all of it. */
.pg-dress path {
  fill: none;
  stroke: #62f3cf;
  stroke-width: 2.5px;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 100 110;
  stroke-dashoffset: 105;
}
.pg-dress path.pg-dress__glow { stroke: rgba(98, 243, 207, 0.3); stroke-width: 7px; }
.pg-dress__tee    { animation: pgDressTee 3.5s linear infinite; }
.pg-dress__sleeve { animation: pgDressSleeve 3.5s linear infinite; }
.pg-dress__shorts { animation: pgDressShorts 3.5s linear infinite; }

/* Pivots are the shoulder points in the shared 170x260 viewBox: (54,79) and (116,79). */
.pg-arm { will-change: transform, opacity; }
.pg-arm--l { transform-origin: 31.765% 30.385%; animation: pgArmL 3.5s linear infinite; }
.pg-arm--r { transform-origin: 68.235% 30.385%; animation: pgArmR 3.5s linear infinite; }

.presence-overlay__text {
  position: relative;
  z-index: 1;
  margin: 0;
  max-width: 24em;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  text-align: center;
  color: #fff;
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55);
}
.presence-overlay__primary {
  font-size: clamp(1rem, 2.6vw, 1.2rem);
  font-weight: 650;
  line-height: 1.4;
}
.presence-overlay__secondary {
  font-size: clamp(0.78rem, 2vw, 0.9rem);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: 0.02em;
  opacity: 0.8;
}
.presence-overlay__secondary[hidden] { display: none; }

@keyframes presenceFadeIn  { from { opacity: 0; } to { opacity: 1; } }
@keyframes presenceFadeOut { from { opacity: 1; } to { opacity: 0; } }

/* One timeline, one duration: every keyframe set below shares the 3.5s cycle and starts
   together (they all start when #presenceOverlay leaves display:none), so percentages
   line up across layers. The eased segments use cubic-bezier(0.4, 0, 0.2, 1); the
   animation itself is linear so a per-keyframe easing is never applied twice. */
@keyframes pgFigure {
  0%   { opacity: 0; }
  5%   { opacity: 0.7; }
  40%  { opacity: 1; }
  90%  { opacity: 1; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  100% { opacity: 0; }
}
@keyframes pgStepBack {
  0%        { transform: translate3d(0, 11%, 0) scale3d(1.35, 1.35, 1); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  40%, 100% { transform: translate3d(0, 0, 0) scale3d(0.85, 0.85, 1); }
}
/* three steps back: up on each stride, down on each footfall */
@keyframes pgWalk {
  0%, 13.333%, 26.667%, 40%, 100% { transform: translate3d(0, 0, 0); animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1); }
  6.667%, 20%, 33.333%            { transform: translate3d(0, -2.2%, 0); animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1); }
}
@keyframes pgFrame {
  0%   { stroke-dashoffset: 0;   stroke: rgba(255, 255, 255, 0.78); }
  40%  { stroke: rgba(255, 255, 255, 0.78); }
  46%  { stroke: rgba(98, 243, 207, 1); }
  90%  { stroke: rgba(98, 243, 207, 0.9); }
  100% { stroke-dashoffset: -24; stroke: rgba(255, 255, 255, 0.78); }
}
@keyframes pgLockGlow {
  0%, 40%  { opacity: 0;   transform: translate3d(0, 0, 0) scale3d(0.97, 0.97, 1); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  46%      { opacity: 1;   transform: translate3d(0, 0, 0) scale3d(1.025, 1.025, 1); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  55%, 90% { opacity: 0.5; transform: translate3d(0, 0, 0) scale3d(1, 1, 1); }
  100%     { opacity: 0;   transform: translate3d(0, 0, 0) scale3d(1, 1, 1); }
}
@keyframes pgLockRing {
  0%, 40%   { opacity: 0;   transform: translate3d(0, 0, 0) scale3d(1, 1, 1); }
  42%       { opacity: 0.9; transform: translate3d(0, 0, 0) scale3d(1, 1, 1); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  56%, 100% { opacity: 0;   transform: translate3d(0, 0, 0) scale3d(1.12, 1.12, 1); }
}
@keyframes pgLinesDim {
  0%, 55%   { opacity: 1; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  70%, 100% { opacity: 0.5; }
}
@keyframes pgDressTee {
  0%, 55%   { stroke-dashoffset: 105; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  67%, 100% { stroke-dashoffset: 0; }
}
@keyframes pgDressSleeve {
  0%, 60%   { stroke-dashoffset: 105; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  70%, 100% { stroke-dashoffset: 0; }
}
@keyframes pgDressShorts {
  0%, 63%   { stroke-dashoffset: 105; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  75%, 100% { stroke-dashoffset: 0; }
}
/* open outward with a small settle - positive is clockwise, which swings a hand hanging
   below its shoulder away from the body on the viewer's left */
@keyframes pgArmL {
  0%, 75%   { transform: translate3d(0, 0, 0) rotate(0deg);  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  83%       { transform: translate3d(0, 0, 0) rotate(16.5deg); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  88%, 100% { transform: translate3d(0, 0, 0) rotate(15deg); }
}
@keyframes pgArmR {
  0%, 75%   { transform: translate3d(0, 0, 0) rotate(0deg);   animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  83%       { transform: translate3d(0, 0, 0) rotate(-16.5deg); animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); }
  88%, 100% { transform: translate3d(0, 0, 0) rotate(-15deg); }
}

/* Reduced motion drops the loop, not the message: a single still of the END state - the
   figure fitted inside a locked frame, dressed - which is the instruction in one picture. */
@media (prefers-reduced-motion: reduce) {
  #presenceOverlay,
  #presenceOverlay.is-leaving,
  .pg-glow, .pg-glow::after, .pg-frame rect, .pg-figure, .pg-zoom, .pg-walk,
  .pg-lines, .pg-dress path, .pg-arm { animation: none; }
  #presenceOverlay.is-leaving { opacity: 0; }
  .pg-figure { opacity: 1; }
  .pg-zoom { transform: scale(0.85); }
  .pg-glow { opacity: 0.5; }
  .pg-frame rect { stroke: rgba(98, 243, 207, 0.9); }
  .pg-lines { opacity: 0.5; }
  .pg-dress path { stroke-dashoffset: 0; }
}

.camera-card #lowerBodyGuard {
  position: absolute; inset: 0; width: 100%; height: 100%;
  z-index: 2; pointer-events: none; transform: none;
  display: none; background: transparent;
}
.camera-card.show-live.lower-body-guard-active #lowerBodyGuard { display: block; }

/* ── Colour swatches - variant bubbles pinned to the camera stage ─────────────
   The only floating gallery control left on the live stage. Pinned to the inline-start
   edge, vertically centred, ABOVE the live #aiVideo (z-index 7 > the video) so the colour
   can be switched mid-stream. Absolutely positioned and out of flow, so the swatch strip
   appearing/disappearing between products NEVER reflows the video stage.
   (The old vertical perspective / AI-mode rail was removed - AI Combined is now the sole,
   hardcoded try-on mode, applied automatically with no on-screen picker.) */
.product-gallery {
  position: absolute;
  inset-inline-start: 12px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 7;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
}

/* Colour swatch strip - a glass pill of round variant bubbles. */
.product-swatches {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 6px 8px;
  border-radius: 999px;
  background: rgba(10, 10, 11, .42);
  -webkit-backdrop-filter: blur(16px) saturate(1.25);
  backdrop-filter: blur(16px) saturate(1.25);
  border: 1px solid rgba(255, 255, 255, .14);
  box-shadow: 0 8px 24px rgba(0, 0, 0, .3);
}
.product-swatches[hidden] { display: none; }
.pg-swatch {
  width: 22px; height: 22px;
  padding: 0;
  border-radius: 50%;
  cursor: pointer;
  background: var(--sw, #8a8f98);
  border: 2px solid rgba(255, 255, 255, .55);
  box-shadow: 0 1px 4px rgba(0, 0, 0, .35);
  transition: transform .2s var(--ease, ease), box-shadow .2s var(--ease, ease),
              border-color .2s var(--ease, ease);
}
.pg-swatch:hover { transform: translateY(-1px) scale(1.06); }
.pg-swatch:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--pear-glow, rgba(141, 182, 0, .5)); }
.pg-swatch.is-active {
  border-color: #8db600;
  box-shadow: 0 0 0 2px #8db600, 0 2px 8px rgba(0, 0, 0, .4);
}

@media (prefers-reduced-motion: reduce) {
  .pg-swatch { transition: none; }
}

/* CLIP replay: a saved history clip plays in #aiVideo (no live billing).
   ── transform:none, DELIBERATELY, and do NOT "fix" this to match show-live ──
   A recorded clip already has the selfie flip BAKED INTO ITS PIXELS by the recorder's
   paint loop (startRecording), because a downloaded file has no CSS to carry it.
   Mirroring it again on replay would un-mirror it, so the shopper's saved clip would
   play back reversed against the live view it was captured from. The two rules differ
   because their sources differ: show-live plays un-mirrored decoded frames and needs the
   flip; show-clip plays already-flipped pixels and must not get a second one. */
.camera-card.show-clip #aiVideo { display: block; transform: none !important; }
.camera-card.show-clip #webcam { visibility: hidden; }
.camera-card.show-clip .camera-card__placeholder { display: none; }
.camera-card.show-clip::before {
  content: "▶ מדידה קודמת"; position: absolute; top: 14px; inset-inline-start: 14px; z-index: 6;
  background: rgba(255,255,255,.85);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  color: var(--ink); font-size: .72rem; font-weight: 800; letter-spacing: .04em;
  padding: 6px 13px; border-radius: var(--pill); border: 1px solid var(--glass-border);
  box-shadow: 0 4px 16px rgba(0,0,0,.18);
}

/* breathing pear-green edge glow while live */
.camera-card.live,
.camera-card.show-live {
  border-color: rgba(141,182,0,.7);
  animation: liveBreath 2.4s var(--ease) infinite;
}
@keyframes liveBreath {
  0%, 100% { box-shadow: 0 0 0 1px rgba(141,182,0,.5), 0 0 22px rgba(141,182,0,.30), var(--shadow); }
  50%      { box-shadow: 0 0 0 2px rgba(141,182,0,.85), 0 0 46px rgba(141,182,0,.52), var(--shadow); }
}

.camera-card__live {
  position: absolute; top: 14px; inset-inline-start: 14px; z-index: 6;
  display: none; align-items: center; gap: 7px;
  background: rgba(255,255,255,.85);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  color: var(--ink); font-size: .72rem; font-weight: 800; letter-spacing: .1em;
  padding: 6px 13px; border-radius: var(--pill); border: 1px solid var(--glass-border);
  box-shadow: 0 4px 16px rgba(0,0,0,.18);
}
.camera-card.live .camera-card__live { display: inline-flex; }
.camera-card.show-result .camera-card__live { display: none; }
.camera-card__live i { width: 7px; height: 7px; border-radius: 50%; background: var(--pear); box-shadow: 0 0 8px var(--pear-glow); animation: blink 1.4s infinite; }
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: .25; } }

/* ── strict 5-second live countdown - the visible token kill-clock ──────────
   A frosted disc whose conic ring drains pear-green → empty as the seconds tick,
   flipping to a pulsing red in the final second. Driven by --cd-frac + .is-final
   from app.js (showLiveCountdown / tickLiveCountdown). */
.live-countdown {
  position: absolute; top: 14px; inset-inline-end: 14px; z-index: 7;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  width: 64px; height: 64px; border-radius: 50%;
  color: #fff; text-align: center; pointer-events: none;
  background:
    radial-gradient(closest-side, rgba(8,8,10,.80) 69%, transparent 71%),
    conic-gradient(var(--pear) calc(var(--cd-frac, 1) * 360deg), rgba(255,255,255,.18) 0);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
  box-shadow: 0 6px 22px rgba(0,0,0,.34), inset 0 0 0 1px rgba(255,255,255,.16);
  animation: cdPop .4s var(--ease) both;
}
.live-countdown[hidden] { display: none; }
.live-countdown__num {
  font-family: var(--font-ui); font-weight: 900; font-size: 1.5rem; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.live-countdown__label {
  font-family: var(--font-ui); font-weight: 700; font-size: .5rem;
  letter-spacing: .05em; opacity: .82; margin-top: 1px;
}
.live-countdown.is-final {
  animation: cdPulse .9s var(--ease) infinite;
  background:
    radial-gradient(closest-side, rgba(40,8,8,.84) 69%, transparent 71%),
    conic-gradient(#ff5a5a calc(var(--cd-frac, 0) * 360deg), rgba(255,255,255,.18) 0);
}
@keyframes cdPop  { from { transform: scale(.55); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@keyframes cdPulse {
  0%, 100% { box-shadow: 0 6px 22px rgba(0,0,0,.34), inset 0 0 0 1px rgba(255,90,90,.45), 0 0 0 0 rgba(255,90,90,.5); }
  50%      { box-shadow: 0 6px 22px rgba(0,0,0,.34), inset 0 0 0 1px rgba(255,90,90,.75), 0 0 0 9px rgba(255,90,90,0); }
}

/* placeholder (camera off) - frosted glass */
.camera-card__placeholder {
  position: absolute; inset: 0; z-index: 7;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px;
  color: var(--ink); text-align: center; padding: 28px;
  background: linear-gradient(160deg, rgba(255,255,255,.72), rgba(238,238,236,.78));
  -webkit-backdrop-filter: var(--blur-lg); backdrop-filter: var(--blur-lg);
}
.camera-card.live .camera-card__placeholder,
.camera-card.show-result .camera-card__placeholder { display: none; }
.camera-card__icon { font-size: 2.7rem; }
.camera-card__placeholder p { margin: 0; font-size: .92rem; color: var(--muted); }
.camera-card__placeholder .btn-primary { width: auto; margin-top: 4px; padding: 13px 32px; }

.camera-card__error {
  position: absolute; left: 14px; right: 14px; bottom: 14px; z-index: 9;
  background: rgba(16,16,18,.80);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  color: #fff; padding: 12px 16px; border: 1px solid rgba(255,255,255,.16);
  border-radius: var(--radius-sm); font-size: .82rem; font-weight: 600; line-height: 1.4;
}
.camera-card__error[hidden] { display: none; }

/* ── Adult/kids size-mismatch card - see updateSizeMismatchUI() in app.js ───── */
.camera-card__size-mismatch {
  position: absolute; left: 14px; right: 14px; bottom: 14px; z-index: 9;
  display: flex; flex-direction: column; align-items: stretch; gap: 10px;
  background: rgba(16,16,18,.85);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  color: #fff; padding: 14px 16px; border: 1px solid rgba(255,255,255,.16);
  border-radius: var(--radius-sm);
}
.camera-card__size-mismatch[hidden] { display: none; }
.camera-card__size-mismatch-text { margin: 0; font-size: .82rem; font-weight: 600; line-height: 1.45; }
.camera-card__size-mismatch-cta { padding: 10px 16px; font-size: .82rem; }

/* A blocked product shows the notice and NOTHING to start: the video/canvas layers and
   the placeholder are all suppressed, so there is no live stage rendered behind the
   card and no Go-Live affordance to reach. The notice itself is re-centred (it is
   absolutely positioned for the normal error case, where it overlays a live feed). */
.camera-card.size-mismatched #webcam,
.camera-card.size-mismatched #aiVideo,
.camera-card.size-mismatched #resultCanvas,
.camera-card.size-mismatched #lowerBodyGuard,
.camera-card.size-mismatched .camera-card__placeholder { display: none !important; }
.camera-card.size-mismatched .camera-card__size-mismatch {
  position: static; margin: 0; align-self: center;
}
.camera-card.size-mismatched {
  display: flex; align-items: center; justify-content: center; padding: 24px;
}

/* ── AI scanning overlay (monochrome sweep with a pear-green bar) ───────── */
.scan-overlay {
  position: absolute; inset: 0; z-index: 8;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
  background: rgba(8,8,10,.34);
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
}
.scan-overlay[hidden] { display: none; }
/* ── THE LOADING WINDOW SHOWED THE BARE BODY (2026-09-19) ───────────────────────────
   REPORTED: "the raw camera is shown before the garment appears". The AI feed is held at
   opacity 0 until Model Ready (gateAiFeed / startBillingWindow), but #webcam - the raw
   preview - is only hidden from .show-live on (see the visibility rules above). For the
   whole loading window (asset preload, token, handshake, warm-up, first verified frame -
   often several seconds) the shopper watched their undressed camera through this
   overlay's 34% scrim and 3px blur - which dims a body, it does not hide one.
   So while the loading overlay is up, the raw preview is frosted to a silhouette: enough
   to stay framed, nothing more. Removed by the same event that removes the overlay - the
   first VERIFIED dressed frame - so there is no gap between the two.
   DISPLAY ONLY, and that is load-bearing (CLAUDE.md §2.9: never pause, gate or hold
   #webcam/localStream). A CSS filter changes nothing about the stream: Decart's input is
   a cloned track drawn by createThrottledInputStream(), and drawImage()/getImageData()
   read the decoded frames, never the filtered paint - so the presence gate, the black-
   screen check and the reveal verification all still see the real camera. */
.camera-card #webcam { transition: filter .45s var(--ease); }
.camera-card:has(> #scanOverlay:not([hidden])) #webcam { filter: blur(28px) saturate(1.1) brightness(1.03); }
.scan-overlay__lines { position: absolute; inset: 0; overflow: hidden; }
.scan-overlay__lines::before {
  content: ""; position: absolute; inset: -50% 0;
  background: repeating-linear-gradient(
    0deg,
    rgba(255,255,255,0) 0px,
    rgba(255,255,255,.16) 2px,
    rgba(255,255,255,0) 4px,
    rgba(255,255,255,0) 9px
  );
  animation: scanPulse 1.6s ease-in-out infinite;
}
.scan-overlay__lines::after {
  content: ""; position: absolute; left: 0; right: 0; height: 16%;
  background: linear-gradient(180deg, rgba(141,182,0,0), rgba(141,182,0,.7), rgba(141,182,0,0));
  filter: blur(2px);
  animation: scanSweep 2.1s cubic-bezier(.5,0,.5,1) infinite;
}
@keyframes scanPulse { 0%,100% { opacity: .35; } 50% { opacity: .9; } }
@keyframes scanSweep { 0% { top: -18%; } 100% { top: 100%; } }

.scan-overlay__text {
  position: relative; z-index: 2;
  display: flex; align-items: center; justify-content: center;
  /* Reserves room for the tallest onboarding step (2 lines on narrow screens in
     either language) so cycling between messages of very different lengths -
     "Fitting active garment physics..." vs "Please step back..." - never nudges
     the loader/timer beneath it. Crossfades via #scanStepText's own opacity
     transition (see .is-fading below), not a height change, so there is no
     layout shift between steps either. */
  min-height: 2.6em; max-width: 320px; margin: 0 auto;
  text-align: center;
  font-family: var(--font-ui); font-weight: 800; font-size: 1.06rem; color: #fff;
  letter-spacing: .01em; text-shadow: 0 2px 16px rgba(0,0,0,.6);
}
#scanStepText { transition: opacity .18s var(--ease); }
#scanStepText.is-fading { opacity: 0; }
.scan-overlay__text .dots i { animation: dot 1.4s infinite; opacity: 0; font-style: normal; }
.scan-overlay__text .dots i:nth-child(2) { animation-delay: .2s; }
.scan-overlay__text .dots i:nth-child(3) { animation-delay: .4s; }
@keyframes dot { 0%,80%,100% { opacity: 0; } 40% { opacity: 1; } }
.scan-overlay__sub {
  position: relative; z-index: 2; font-size: .76rem; font-weight: 600;
  color: rgba(255,255,255,.85); letter-spacing: .06em; text-transform: uppercase;
  font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) {
  #scanStepText { transition: none; }
}

/* ── live try-on controls (premium pills) ──────────────────────────────── */
.cam-controls {
  display: flex; flex-direction: column; align-items: center; gap: 14px;
  max-width: 470px; margin: 24px auto 0;
}
/* ── Permanent clearance from the fixed help button ────────────────────────
   #captureBtn (in .cam-controls) and #addToCartBtn (in .add-cart-row, just
   below it) are both width:100% of this row, and the row itself has no `width`
   of its own - only max-width:470px - so on any phone narrower than that it
   simply fills the container's full content width, edge to edge. The help
   button is fixed at inset-inline-start:20px (14px ≤480px), so on that exact
   viewport range (~390-430px, where the container's own edge padding leaves
   just enough - and not quite enough - room) the button's corner and this
   row's corner land in the same pixels: measured 33×38px of REAL overlap at
   430px, most of which the round help button actually wins.
   This is deliberately a static, unconditional reservation, not a runtime
   check - a button that dynamically fades in and out as content scrolls past
   it reads as broken (that was this file's previous approach, replaced after
   it was reported as constant "popping on and off"). Reserving the corner
   permanently makes the overlap geometrically impossible at every scroll
   position, not just probabilistically unlikely at some of them. */
:root {
  --help-fab-inset: 20px; --help-fab-size: 44px; --help-fab-clear: 10px;
  --help-fab-gutter: calc(var(--help-fab-inset) + var(--help-fab-size) + var(--help-fab-clear));
}
@media (max-width: 480px) {
  :root { --help-fab-inset: 14px; --help-fab-size: 38px; --help-fab-clear: 8px; }
}
@media (max-width: 600px) {
  /* BOTH sides, not just padding-inline-start: these rows are centered via
     margin:auto on their OWN outer box (see .cam-controls/.add-cart-row
     above), and their single child button is width:100% of that box - so
     padding on only ONE side does clear the help button, but it also eats
     into the available width from just that side, dragging the (still
     "centered" as a box) row's CONTENT off dead-center. Measured: a real
     60-74px shift, the two buttons landing a third of their own width off
     from where they belong. Matching padding-inline-end keeps the button
     centered on the page exactly as before, while the still-present
     start-side padding is what actually keeps this clear of the button -
     inline-start uses the logical property for the same reason as before
     (tracks direction automatically); inline-end doesn't need to, since it's
     restoring symmetry, not chasing the fixed control's own corner. */
  .cam-controls, .add-cart-row {
    padding-inline-start: var(--help-fab-gutter);
    padding-inline-end: var(--help-fab-gutter);
    box-sizing: border-box;
  }
}
/* "Go Live" - inviting translucent pear-green glass capsule */
.btn-capture {
  width: 100%; display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 16px 24px; border: 1px solid rgba(141, 182, 0, .35); border-radius: var(--pill);
  background: var(--pear-tint);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--pear-deep); cursor: pointer;
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, .55);
  transition: all .4s var(--ease);
}
.btn-capture:hover:not(:disabled) {
  transform: translateY(-2px);
  background: rgba(141, 182, 0, .26); border-color: rgba(141, 182, 0, .55);
  box-shadow: 0 12px 32px rgba(141, 182, 0, .22), inset 0 1px 0 rgba(255, 255, 255, .6);
}
.btn-capture:active:not(:disabled) { transform: scale(.96); }
.btn-capture:disabled {
  background: rgba(255,255,255,.3);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--muted-2); border-color: var(--glass-line); box-shadow: none; cursor: not-allowed;
}
/* "Stop" - clean monochrome black glass capsule */
.btn-capture.is-live {
  background: var(--glass-dark);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: #fff; border-color: rgba(255, 255, 255, .18);
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, .12); animation: none;
}
.btn-capture.is-live:hover:not(:disabled) {
  transform: translateY(-2px);
  background: rgba(16, 16, 18, .74); border-color: rgba(255, 255, 255, .3);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .22);
}
.btn-capture.is-live:active:not(:disabled) { transform: scale(.96); }

/* ── go-live gate: "still finding/generating a back view" ─────────────────────
   The FUNCTIONAL gate is livePendingReason() inside goLive() itself (a click here
   is rejected with a toast regardless of this class ever applying) - this is the
   visual half, so a shopper sees WHY the button won't respond instead of tapping
   it and getting only a toast. Deliberately independent of the :disabled attribute
   (see syncCaptureButtonPendingState()'s comment in app.js for why) - own
   pointer-events:none is what actually blocks the click at the CSS layer, own
   opacity/cursor mimic the :disabled look without touching that attribute's
   several other independent owners. `:not(.is-pending-back)` on the pre-existing
   hover/active rules above would also work but is more invasive to touch; higher
   specificity here wins the same result additively. */
.btn-capture.is-pending-back {
  pointer-events: none;
  opacity: .55;
  cursor: not-allowed;
  animation: none !important;   /* stop the mobile "capInvite" attention pulse while waiting */
}
.btn-capture.is-pending-back:hover,
.btn-capture.is-pending-back:active {
  transform: none;
}
.btn-capture.is-pending-back .btn-capture__icon {
  position: relative;
  visibility: hidden;   /* keep the layout box; the spinner below replaces the icon */
}
.btn-capture.is-pending-back .btn-capture__icon::after {
  content: "";
  position: absolute; top: 50%; left: 50%; visibility: visible;
  width: 16px; height: 16px; margin: -8px 0 0 -8px;
  border-radius: 50%;
  border: 2px solid rgba(10, 10, 11, .18);
  border-top-color: var(--pear-deep);
  animation: pear-pending-spin .8s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .btn-capture.is-pending-back .btn-capture__icon::after { animation: none; }
}

.btn-capture__icon { font-size: 1.2rem; }
.btn-capture__label { font-family: "Urbanist"; font-weight: 800; font-size: 1.05rem; }
.btn-capture__en { font-size: .74rem; opacity: .85; letter-spacing: .06em; }
.btn-ghost[hidden] { display: none; }

/* ── product media framing (unchanged behaviour) ───────────────────────── */
.active-garment__media,
.cat-item__media,
.cl-card__media { position: relative; overflow: hidden; }

.active-garment__media > span,
.cat-item__media > span,
.cl-card__media > span,
.active-garment__media svg, .active-garment__media img, .active-garment__media video,
.cat-item__media svg,       .cat-item__media img,       .cat-item__media video,
.cl-card__media svg,        .cl-card__media img,        .cl-card__media video {
  display: block; width: 100%; height: 100%;
}
.active-garment__media svg,
.cat-item__media svg,
.cl-card__media svg { object-fit: contain; }
.active-garment__media img, .active-garment__media video,
.cat-item__media img,       .cat-item__media video,
.cl-card__media img,        .cl-card__media video {
  object-fit: cover;
  object-position: center top;
  will-change: transform;
  transition: transform .5s var(--ease);
}
.cat-item:hover .cat-item__media img,
.cat-item:hover .cat-item__media video,
.cl-card:hover .cl-card__media img,
.cl-card:hover .cl-card__media video {
  transform: scale(1.06);
}

/* ── catalog panel - floating glass "bubble" overlay holding glass tiles ── */
.catalog-panel,
.garment-selector-bubble {
  margin-top: 46px; padding: 22px clamp(16px, 4vw, 26px);
  background: var(--glass);
  -webkit-backdrop-filter: var(--blur-lg); backdrop-filter: var(--blur-lg);
  border: 1px solid var(--glass-border); border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  transition: box-shadow .4s var(--ease), transform .4s var(--ease);
}
.catalog-panel[hidden],
.garment-selector-bubble[hidden] { display: none; }
.catalog-title { font-size: 1.14rem; font-weight: 800; margin: 0 0 18px; }
.catalog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 14px; }
.cat-item {
  border: 1px solid var(--glass-border); border-radius: var(--radius-sm); overflow: hidden;
  background: var(--glass);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  box-shadow: var(--shadow-soft);
  cursor: pointer;
  transition: border-color .3s var(--ease), box-shadow .4s var(--ease), transform .4s var(--ease);
}
.cat-item:hover { border-color: rgba(141,182,0,.5); box-shadow: var(--shadow-lift); transform: translateY(-4px) scale(1.02); }
.cat-item:active { transform: scale(.96); }
.cat-item.is-active {
  border-color: var(--pear);
  box-shadow: 0 0 0 2px var(--pear-ring), var(--shadow);
  animation: tilePulse 2s var(--ease) infinite;
}
@keyframes tilePulse {
  0%,100% { box-shadow: 0 0 0 2px rgba(141,182,0,.18), var(--shadow); }
  50%     { box-shadow: 0 0 0 4px rgba(141,182,0,.30), 0 0 24px rgba(141,182,0,.30); }
}
.cat-item__media { position: relative; aspect-ratio: 3 / 4; background: rgba(255,255,255,.32); overflow: hidden; }
.cat-item__body { padding: 10px 12px 12px; }
.cat-item__name { display: block; font-weight: 700; font-size: .82rem; color: var(--ink); }
.cat-item__price { font-size: .78rem; color: var(--muted); }

/* ── two-view (front/back) completeness badge ────────────────────────────────
   Top-right pill on each catalog card. Two dots (front · back) fill in when a real
   image ships for that view; a ✓ marks fully-documented items, a 🔒 marks an
   opt-in strict item still missing its back. */
.viewbadge {
  position: absolute; top: 7px; right: 7px; z-index: 2;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 6px; border-radius: 999px;
  background: rgba(20,22,26,.58); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
  box-shadow: 0 1px 4px rgba(0,0,0,.22);
  line-height: 1; pointer-events: none;
}
.viewbadge__dot {
  width: 6px; height: 6px; border-radius: 50%;
  border: 1.5px solid rgba(255,255,255,.85); background: transparent;
}
.viewbadge__dot.is-on { background: #fff; }
.viewbadge__mark { font-size: .62rem; font-weight: 800; color: #fff; margin-left: 1px; }
.viewbadge--complete { background: rgba(46,120,58,.72); }
.viewbadge--blocked  { background: rgba(150,40,40,.74); }
.cat-item--blocked { opacity: .82; }
.cat-item--blocked .cat-item__media { filter: grayscale(.28); }

/* ── complete the look - glass cards ───────────────────────────────────── */
.complete-look { margin-top: 48px; }
.complete-look[hidden] { display: none; }
.complete-look__head { margin-bottom: 18px; }
.cl-title { font-size: 1.2rem; font-weight: 800; margin: 0; }
.cl-title span:not([data-i18n]) { color: var(--pear-deep); font-weight: 700; }
.cl-sub { margin: 5px 0 0; color: var(--muted); font-size: .9rem; }

/* Non-scrolling positioning reference for the arrows below - ONLY #clTrack
   itself scrolls, so the arrows (its siblings here) stay pinned to the
   carousel's edges regardless of how far the cards have scrolled, instead of
   scrolling away with the content. */
.cl-track-wrap { position: relative; }

.cl-track {
  display: flex; gap: 16px; overflow-x: auto; padding: 4px 2px 18px;
  scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.cl-track::-webkit-scrollbar { height: 0; width: 0; display: none; }

/* Desktop-only mouse/trackpad scroll arrows - same pear-tint/pear-deep glass-pill
   language as .cl-card__look ("Add to Look") so they read as part of the same
   design system rather than a bolted-on control. display:none by default (touch
   devices already have native horizontal swipe); enabled only at desktop widths
   in the @media block below. */
.cl-arrow {
  display: none;
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 2;
  width: 44px; height: 44px; border-radius: 50%;
  align-items: center; justify-content: center;
  border: 1px solid rgba(141, 182, 0, .35);
  background: var(--pear-tint);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--pear-deep);
  font-size: 1.4rem; line-height: 1; cursor: pointer;
  box-shadow: var(--shadow-soft);
  transition: transform .3s var(--ease), background .3s var(--ease),
              border-color .3s var(--ease), box-shadow .3s var(--ease);
}
.cl-arrow:hover {
  transform: translateY(-50%) scale(1.05);
  background: rgba(141, 182, 0, .26); border-color: rgba(141, 182, 0, .55);
  box-shadow: 0 8px 22px rgba(141, 182, 0, .2);
}
.cl-arrow:active { transform: translateY(-50%) scale(.94); }
.cl-arrow svg { flex: none; }
/* Plain left/right (not inset-inline-start/end): these buttons scroll the
   PHYSICAL viewport left/right (see the JS - scrollBy({left}) is a physical-axis
   API regardless of RTL), so "left arrow" must always be the button on the
   visual left, independent of the page's RTL reading direction.
   NEGATIVE offsets - push the buttons OUTSIDE .cl-track-wrap's own box (which
   exactly matches the visible card area) instead of overlapping the first/last
   card. .complete-look/.container have no overflow:hidden in their ancestor
   chain, so this negative offset is never clipped. */
.cl-arrow--prev { left: -55px; }
.cl-arrow--next { right: -55px; }

/* Desktop only. Mouse/trackpad users benefit from click-to-scroll; touch
   devices already scroll the track natively via swipe, so these would just be
   redundant floating buttons that can be mis-tapped over the cards. */
@media (min-width: 1024px) {
  .cl-arrow { display: flex; }
}

.cl-card {
  flex: 0 0 182px; scroll-snap-align: start;
  border: 1px solid var(--glass-border); border-radius: var(--radius); overflow: hidden;
  background: var(--glass);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  box-shadow: var(--shadow-soft);
  display: flex; flex-direction: column;
  transition: border-color .3s var(--ease), box-shadow .4s var(--ease), transform .4s var(--ease);
  animation: fade .5s both; animation-delay: calc(var(--i,0) * .06s);
}
.cl-card:hover { border-color: rgba(141,182,0,.5); box-shadow: var(--shadow-lift); transform: translateY(-5px) scale(1.02); }
.cl-card:active { transform: scale(.97); }
.cl-card__media { aspect-ratio: 3/4; background: rgba(255,255,255,.32); }
.cl-card__body { padding: 12px 14px 4px; flex: 1; }
.cl-card__cat { display: block; font-size: .68rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--pear-deep); }
.cl-card__name { font-family: var(--font-ui); font-size: .98rem; font-weight: 800; margin: 4px 0; color: var(--ink); }
.cl-card__price { font-size: .84rem; color: var(--ink); font-weight: 600; }
/* "Complete the Look" CTA - translucent pear-green glass capsule */
.cl-card__look {
  margin: 10px; padding: 11px 14px; border: 1px solid rgba(141, 182, 0, .35); border-radius: var(--pill);
  background: var(--pear-tint);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--pear-deep);
  font-weight: 800; font-size: .82rem; letter-spacing: .01em;
  cursor: pointer; box-shadow: inset 0 1px 0 rgba(255, 255, 255, .5);
  transition: all .4s var(--ease);
}
.cl-card__look:hover { transform: translateY(-2px); background: rgba(141, 182, 0, .26); border-color: rgba(141, 182, 0, .55); box-shadow: 0 10px 24px rgba(141, 182, 0, .2); }
.cl-card__look:active { transform: scale(.96); }

/* full-look confirmation cue */
.complete-look.is-complete .cl-title { position: relative; }
.complete-look.is-complete .cl-title::after {
  content: "✓ Total Look"; margin-inline-start: 10px; font-size: .68rem; font-weight: 800;
  letter-spacing: .04em; text-transform: uppercase; color: #fff;
  background: linear-gradient(135deg, var(--pear-deep), var(--pear));
  padding: 3px 11px; border-radius: var(--pill); vertical-align: middle;
  box-shadow: 0 4px 14px rgba(141,182,0,.3);
}

/* ── toast ─────────────────────────────────────────────────────────────── */
.toast {
  position: fixed; left: 50%; bottom: 28px; transform: translate(-50%, 18px);
  background: var(--glass-dark);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: #fff; padding: 14px 24px; border-radius: var(--pill);
  border: 1px solid rgba(255,255,255,.18);
  font-size: .9rem; font-weight: 600; opacity: 0; pointer-events: none;
  box-shadow: var(--shadow-lift); z-index: 100;
  transition: opacity .3s var(--ease), transform .3s var(--ease);
  max-width: 90vw;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast b { color: var(--pear); text-decoration: underline; text-underline-offset: 2px; }

@media (max-width: 480px) {
  .personal-title { font-size: 1.35rem; }
  .container h2 { font-size: 1.6rem; }
}

/* ── "Watch Again" + "Download Video" capsule row ──────────────────────── */
.post-session-actions {
  display: flex; gap: 12px; width: 100%;
}
.post-session-actions[hidden] { display: none; }
.post-session-actions .btn-watch,
.post-session-actions .btn-download { flex: 1; width: auto; min-width: 0; }

/* "Watch Again" - translucent pear-green glass capsule (primary replay) */
.btn-watch {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 14px 24px; border: 1px solid rgba(141, 182, 0, .35); border-radius: var(--pill);
  background: var(--pear-tint);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--pear-deep); cursor: pointer;
  font-weight: 800; font-family: "Urbanist", sans-serif; font-size: 1rem;
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, .55);
  transition: all .4s var(--ease);
}
.btn-watch:hover { transform: translateY(-2px); background: rgba(141, 182, 0, .26); border-color: rgba(141, 182, 0, .55); box-shadow: 0 12px 32px rgba(141, 182, 0, .22); }
.btn-watch:active { transform: scale(.96); }
.btn-watch__icon { font-size: 1.15rem; }
.btn-watch__en { font-size: .74rem; opacity: .8; letter-spacing: .06em; }

/* "Download Video" - frosted monochrome glass capsule (secondary save) */
.btn-download {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 14px 24px; border: 1px solid var(--glass-border); border-radius: var(--pill);
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--ink); cursor: pointer; font-weight: 800;
  font-family: "Urbanist", sans-serif; font-size: 1rem; box-shadow: var(--shadow-soft);
  transition: transform .4s var(--ease), box-shadow .4s var(--ease), background .3s var(--ease);
}
.btn-download:hover { transform: translateY(-3px) scale(1.02); background: rgba(255,255,255,.85); box-shadow: var(--shadow); }
.btn-download:active { transform: scale(.96); }
.btn-download__icon { font-size: 1.15rem; }
.btn-download__en { font-size: .74rem; opacity: .8; letter-spacing: .06em; }

/* ════════════════════════════════════════════════════════════════════════
   "Finished height + weight" - cool cascading reveal of the optional fields
   ─────────────────────────────────────────────────────────────────────────
   When both mandatory fields become valid, app.js adds .is-expanded. The panel
   itself stretches open (above); on top of that each optional row pops up in
   sequence (slight elastic overshoot) so the form feels like it is "filling
   up", inviting the user to keep going - or to continue. ░░ */
.optional-fields.is-expanded > * {
  animation: pearPopIn .55s cubic-bezier(.34, 1.56, .64, 1) both;
}
/* delays start later so the fine-tune tray clearly lands AFTER the result + button */
.optional-fields.is-expanded > *:nth-child(1) { animation-delay: .26s; }
.optional-fields.is-expanded > *:nth-child(2) { animation-delay: .35s; }
.optional-fields.is-expanded > *:nth-child(3) { animation-delay: .44s; }
.optional-fields.is-expanded > *:nth-child(4) { animation-delay: .53s; }
@keyframes pearPopIn {
  0%   { opacity: 0; transform: translateY(14px) scale(.96); }
  60%  { opacity: 1; transform: translateY(-2px) scale(1.01); }
  100% { opacity: 1; transform: none; }
}
/* the recommended size lands with a satisfying pop, then a slow pear sheen */
.result-box.show .size-display {
  animation: sizePop .5s cubic-bezier(.34, 1.56, .64, 1) both,
             sizeSheen 3.4s ease-in-out .5s infinite;
}
@keyframes sizePop {
  0%   { transform: scale(.7); opacity: 0; }
  60%  { transform: scale(1.08); opacity: 1; }
  100% { transform: scale(1); }
}
@keyframes sizeSheen {
  0%, 100% { background-position: 0% 50%; }
  50%      { background-position: 100% 50%; }
}
/* ── Inverted hierarchy: Result + Continue tray sits directly below Weight ──
   Idle → fully collapsed (height 0). When app.js adds .is-ready it expands with
   a fluid elastic curve; the size box then the button stagger into place. ░░ */
.result-actions {
  max-height: 0; opacity: 0; visibility: hidden; overflow: hidden; margin-top: 0;
  transform: translateY(-10px) scale(.985);
  transition:
    max-height .6s cubic-bezier(.25, 1, .5, 1),
    opacity .4s ease-out,
    transform .5s cubic-bezier(.175, .885, .32, 1.1),
    margin-top .6s cubic-bezier(.25, 1, .5, 1),
    visibility 0s linear .6s;
}
/* 420px → 860px (2026-09-18): the tray now also carries the terms & accuracy
   consent panel (#consentPanel) between the size and Continue. Measured scrollHeight
   at a 320px viewport with the original TWO-box panel: 668px Hebrew, 709px English.
   The one-row consent (2026-09-19) is shorter, so the ceiling only gained headroom;
   it is kept rather than re-tuned to a tighter number that could clip on zoom. max-height is a CEILING, not a
   height, so the extra headroom cannot open a gap - but overflow:hidden CLIPS, and a
   clipped consent box would hide the very checkbox that unlocks Continue. */
.result-actions.is-ready {
  max-height: 860px; opacity: 1; visibility: visible;
  transform: translateY(0) scale(1); margin-top: 10px;
  transition:
    max-height .6s cubic-bezier(.25, 1, .5, 1),
    opacity .4s ease-out .05s,
    transform .5s cubic-bezier(.175, .885, .32, 1.1),
    margin-top .6s cubic-bezier(.25, 1, .5, 1),
    visibility 0s;
}   /* +16px grid gap ≈ 26px */
.result-actions .result-box { margin-top: 0; transition-delay: .06s; }   /* tray owns the top gap */

/* staggered liquid entry - the size box first, the Continue button just after */
#btn-next-screen {
  opacity: 0; transform: translateY(12px) scale(.98);
  /* This ID selector outranks .btn-primary's `transition: all .4s var(--ease)` for the
     WHOLE `transition` property (ID beats class - the cascade doesn't merge the two
     lists, it replaces one with the other). `color` was missing from this list, so the
     text colour (grey ↔ pear-green on disabled/enabled) snapped instantly while the
     pill's background/border/glow eased in - a jarring, out-of-sync flash on the exact
     moment the button becomes clickable. Included here to keep every property in sync. */
  transition: opacity .6s cubic-bezier(.25, 1, .5, 1),
              transform .6s cubic-bezier(.25, 1, .5, 1),
              background .4s var(--ease), border-color .4s var(--ease),
              box-shadow .4s var(--ease), color .4s var(--ease);
  transition-delay: .18s;
}
#btn-next-screen:not(:disabled) { opacity: 1; transform: none; }
#btn-next-screen:hover:not(:disabled)  { transform: translateY(-2px); }   /* keep id-level hover/press */
#btn-next-screen:active:not(:disabled) { transform: scale(.96); }

/* once a size exists the Continue button breathes a soft pear glow - "you can go" */
#btn-next-screen:not(:disabled) { animation: continueInvite 2.6s ease-in-out infinite; }
#btn-next-screen:hover:not(:disabled),
#btn-next-screen:focus-visible:not(:disabled) { animation: none; }
@keyframes continueInvite {
  0%, 100% { box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,.55); }
  50%      { box-shadow: 0 12px 30px rgba(141,182,0,.30), 0 0 0 4px rgba(141,182,0,.12), inset 0 1px 0 rgba(255,255,255,.6); }
}

/* ════════════════════════════════════════════════════════════════════════
   🍐 PEAR LOADER - shown whenever the app is loading something
   A juicy pear bounces with squash-&-stretch over a soft contact shadow.
   Used inside #scanOverlay (go-live render) and the camera-start overlay.
   ════════════════════════════════════════════════════════════════════════ */
.pear-loader {
  position: relative; z-index: 2;
  display: flex; flex-direction: column; align-items: center; gap: 10px;
}
.pear-loader__fruit {
  font-size: 2.8rem; line-height: 1;
  transform-origin: 50% 100%;
  filter: drop-shadow(0 8px 10px rgba(0,0,0,.22));
  animation: pearBounce 1.05s cubic-bezier(.5, .05, .5, .95) infinite;
}
@keyframes pearBounce {
  0%   { transform: translateY(0)     scale(1, 1); }
  15%  { transform: translateY(2px)   scale(1.12, .88); }  /* squash on landing */
  45%  { transform: translateY(-30px) scale(.92, 1.08); }  /* stretch at apex   */
  70%  { transform: translateY(0)     scale(1.08, .92); }
  100% { transform: translateY(0)     scale(1, 1); }
}
.pear-loader__shadow {
  width: 40px; height: 8px; border-radius: 50%;
  background: rgba(0,0,0,.28); filter: blur(2px);
  animation: pearShadow 1.05s cubic-bezier(.5, .05, .5, .95) infinite;
}
@keyframes pearShadow {
  0%, 100% { transform: scale(1);   opacity: .5; }
  45%      { transform: scale(.55); opacity: .22; }
}
.pear-loader__label {
  font-size: .82rem; font-weight: 600; letter-spacing: .04em;
  color: var(--muted);
}
/* on the dark scan overlay the label reads light */
.scan-overlay .pear-loader__label { color: rgba(255,255,255,.85); }
.scan-overlay .pear-loader { margin-bottom: 4px; }

/* full-cover frosted pear overlay used while the camera is opening */
.pear-cam-loader {
  position: absolute; inset: 0; z-index: 8;
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(160deg, rgba(255,255,255,.72), rgba(238,238,236,.8));
  -webkit-backdrop-filter: var(--blur-lg); backdrop-filter: var(--blur-lg);
  animation: fade .3s var(--ease) both;
}

/* ════════════════════════════════════════════════════════════════════════
   PEAR Live-Action Gallery & Retake Pod - unified liquid-glass capsule (Screen 2)
   A single floating pod: Retake capsule + a horizontal swipeable tray of
   "Live Photo" try-on clips (local 5s blobs, poster-backed for persistence).
   ════════════════════════════════════════════════════════════════════════ */
.pear-interaction-pod {
  max-width: 760px; margin: 42px auto 8px;
  padding: 20px clamp(16px, 4vw, 22px);
  background: linear-gradient(135deg, rgba(255,255,255,.35) 0%, rgba(255,255,255,.14) 100%);
  -webkit-backdrop-filter: blur(25px) saturate(210%); backdrop-filter: blur(25px) saturate(210%);
  border: 1px solid rgba(255,255,255,.45); border-radius: 28px;
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,.5);
  animation: galleryPodIn .6s cubic-bezier(.25, 1, .5, 1) both;
}
.pear-interaction-pod[hidden] { display: none; }
@keyframes galleryPodIn { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }

/* ── pod header: icon · title · Retake capsule · clear ── */
.pip-head { display: flex; align-items: center; gap: 10px; margin: 0 2px 16px; }
.pip-icon { display: inline-flex; color: var(--pear-deep); }
.pip-icon svg { width: 18px; height: 18px; }
.pip-title { font-size: 1.04rem; font-weight: 800; margin: 0; }
.pip-title span:not([data-i18n]) { color: var(--pear-deep); font-weight: 700; font-size: .84rem; }

/* Apple glass-pill Retake button - transparent, frosted reflection, ↺ icon */
.pip-retake {
  margin-inline-start: auto;
  display: inline-flex; align-items: center; gap: 7px;
  padding: 9px 16px; border-radius: 50px;
  background: rgba(255,255,255,.28);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  border: 1px solid var(--glass-border);
  color: var(--ink); font-family: "Urbanist", sans-serif; font-weight: 800; font-size: .86rem;
  cursor: pointer; box-shadow: var(--shadow-soft), inset 0 1px 0 rgba(255,255,255,.6);
  transition: all .5s cubic-bezier(.16, 1, .3, 1);
}
.pip-retake:hover { transform: translateY(-2px); background: rgba(255,255,255,.55); border-color: rgba(141,182,0,.45); box-shadow: var(--shadow); }
.pip-retake:active { transform: scale(.95); }     /* fluid finger-press resistance */
.pip-retake__icon { font-size: 1.05rem; line-height: 1; color: var(--pear-deep); transition: transform .6s cubic-bezier(.16,1,.3,1); }
.pip-retake:hover .pip-retake__icon { transform: rotate(-180deg); }
.pip-retake__en { font-size: .68rem; font-weight: 600; opacity: .6; letter-spacing: .04em; }
.pip-retake[hidden] { display: none; }

.pip-clear {
  font-size: .74rem; font-weight: 700; color: var(--muted);
  background: var(--glass-soft); border: 1px solid var(--glass-border); border-radius: var(--pill);
  padding: 6px 13px; cursor: pointer; transition: all .4s var(--ease);
}
.pip-clear:hover { color: var(--ink); background: rgba(255,255,255,.7); transform: translateY(-1px); }
.pip-clear:active { transform: scale(.96); }

/* ── horizontal swipe tray ── */
.pear-gallery-track {
  display: flex; gap: 14px; overflow-x: auto; padding: 4px 2px 6px;
  scroll-snap-type: x proximity; -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.pear-gallery-track::-webkit-scrollbar { height: 0; width: 0; display: none; }
.pear-gallery-track:empty { display: none; }

/* ── Live Photo tile ── */
.live-gallery-item {
  position: relative; flex: 0 0 138px; scroll-snap-align: start;
  display: block; padding: 0; text-align: start; cursor: pointer;
  border: 1px solid var(--glass-border); border-radius: 16px; overflow: hidden;
  background: var(--glass);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  box-shadow: var(--shadow-soft);
  transition: transform .5s cubic-bezier(.16, 1, .3, 1), box-shadow .5s cubic-bezier(.16, 1, .3, 1), border-color .4s var(--ease);
  animation: galleryItemIn .5s cubic-bezier(.25, 1, .5, 1) both;
  animation-delay: calc(var(--gi, 0) * .05s);
}
.live-gallery-item:hover { transform: scale(1.06) translateY(-4px); box-shadow: var(--shadow-lift); border-color: rgba(141,182,0,.55); }
.live-gallery-item:active { transform: scale(.95); }   /* organic fluid press */
/* a tiny "LIVE" dot marks tiles that hold a playable clip */
.live-gallery-item.has-clip::before {
  content: ""; position: absolute; top: 9px; inset-inline-start: 9px; z-index: 3;
  width: 7px; height: 7px; border-radius: 50%; background: var(--pear);
  box-shadow: 0 0 0 3px rgba(141,182,0,.25), 0 0 10px var(--pear-glow);
  animation: blink 1.6s infinite;
}
@keyframes galleryItemIn { from { opacity: 0; transform: translateY(14px) scale(.96); } to { opacity: 1; transform: none; } }

.live-gallery-item__media {
  position: relative; display: block; aspect-ratio: 3 / 4; background: #050505; overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.08), inset 0 2px 16px rgba(0,0,0,.4);
}
/* video + poster image both fully cover the tile like a high-res app badge */
.live-gallery-item__media video,
.live-gallery-item__media img {
  width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 16px;
}

/* absolute blurred micro-badge: "Name - Size" */
.live-gallery-item__badge {
  position: absolute; left: 8px; right: 8px; bottom: 8px; z-index: 2;
  display: flex; align-items: center; gap: 7px;
  padding: 6px 10px; border-radius: 12px;
  background: rgba(16,16,18,.5);
  -webkit-backdrop-filter: blur(5px) saturate(180%); backdrop-filter: blur(5px) saturate(180%);
  border: 1px solid rgba(255,255,255,.18);
}
.live-gallery-item__name {
  flex: 1; min-width: 0; color: #fff; font-size: .7rem; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.live-gallery-item__size {
  flex-shrink: 0; font-family: var(--font-ui); font-size: .74rem; font-weight: 900;
  letter-spacing: .02em; color: var(--pear);
}

/* tap-to-view lightbox - full-size glass overlay (image or looping clip) */
.pear-lightbox { position: fixed; inset: 0; z-index: 200; display: none; align-items: center; justify-content: center; padding: 24px; }
.pear-lightbox.show { display: flex; animation: fade .35s var(--ease) both; }
.pear-lightbox__backdrop {
  position: absolute; inset: 0; background: rgba(10,10,12,.55);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
}
.pear-lightbox__fig {
  position: relative; z-index: 1; margin: 0; max-width: min(460px, 92vw); max-height: 84vh;
  display: flex; flex-direction: column; gap: 12px;
  animation: galleryItemIn .45s cubic-bezier(.25, 1, .5, 1) both;
}
.pear-lightbox__img, .pear-lightbox__video {
  width: 100%; max-height: 74vh; object-fit: contain; border-radius: 22px;
  border: 1px solid var(--glass-border); box-shadow: var(--shadow-lift); background: #050505;
}
.pear-lightbox__cap { text-align: center; color: #fff; font-family: var(--font-ui); font-weight: 800; font-size: 1rem; letter-spacing: .02em; }
.pear-lightbox__close {
  position: absolute; top: -8px; inset-inline-end: -8px; z-index: 2;
  width: 40px; height: 40px; border-radius: 50%;
  border: 1px solid var(--glass-border); background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--ink); font-size: 1.5rem; line-height: 1; cursor: pointer;
  transition: all .4s var(--ease);
}
.pear-lightbox__close:hover { transform: scale(1.08); background: #fff; }
.pear-lightbox__close:active { transform: scale(.94); }

/* ── large interactive fit modal (Image 1): name + size badge + action row ── */
.pear-lightbox__fig { gap: 14px; }
.pear-lightbox__img, .pear-lightbox__video { max-height: 62vh; }   /* leave room for caption + actions */
.pear-lightbox__cap {
  display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap;
}
.pear-lightbox__name {
  font-family: var(--font-ui); font-weight: 800; font-size: 1.05rem; letter-spacing: .02em; color: #fff;
}
.pear-lightbox__size {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 30px; height: 26px; padding: 0 10px;
  border-radius: var(--pill); background: var(--pear); color: #0a0a0b;
  font-family: var(--font-ui); font-weight: 900; font-size: .82rem; letter-spacing: .04em;
  box-shadow: var(--shadow-pear);
}
.pear-lightbox__actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.plb-btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 11px 18px; border-radius: var(--pill); cursor: pointer;
  font-family: "Urbanist", sans-serif; font-weight: 800; font-size: .9rem;
  border: 1px solid rgba(255,255,255,.5);
  background: rgba(255,255,255,.16);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  color: #fff; text-decoration: none;
  transition: transform .35s var(--ease), background .35s var(--ease), box-shadow .35s var(--ease);
}
.plb-btn:hover  { transform: translateY(-2px); background: rgba(255,255,255,.28); }
.plb-btn:active { transform: translateY(0) scale(.97); }
.plb-btn[hidden] { display: none; }
.plb-btn__icon { font-size: 1rem; line-height: 1; }
.plb-btn__en   { font-size: .68rem; font-weight: 700; opacity: .72; letter-spacing: .04em; }
.plb-btn--live {
  background: var(--pear); color: #0a0a0b; border-color: transparent; box-shadow: var(--shadow-pear);
}
.plb-btn--live:hover { background: #9cca00; }
.plb-btn--live .plb-btn__en { opacity: .6; }

/* ── active replay glow - the tile currently playing in #aiVideo ── */
.live-gallery-item.is-playing {
  border-color: var(--pear);
  box-shadow: 0 0 0 2px var(--pear-ring), 0 10px 28px rgba(141,182,0,.3), var(--shadow-lift);
  animation: tilePulse 2s var(--ease) infinite;
}

/* ── multi-select toggle for Compare mode ── */
.lgi-select {
  position: absolute; top: 8px; inset-inline-end: 8px; z-index: 4;
  width: 24px; height: 24px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1.5px solid rgba(255,255,255,.8); background: rgba(16,16,18,.4);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  color: transparent; font-size: .78rem; font-weight: 900; line-height: 1; cursor: pointer;
  transition: all .5s cubic-bezier(.16, 1, .3, 1);
}
.lgi-select:hover { transform: scale(1.12); background: rgba(16,16,18,.6); }
.lgi-select:active { transform: scale(.9); }
.live-gallery-item.is-selected { border-color: var(--pear); box-shadow: 0 0 0 2px rgba(141,182,0,.4), var(--shadow); }
.live-gallery-item.is-selected .lgi-select {
  background: var(--pear); border-color: #fff; color: #fff;
  box-shadow: 0 0 0 3px rgba(141,182,0,.3), 0 0 12px var(--pear-glow);
}

/* ── floating "Compare Looks" pill (appears when exactly 2 are selected) ── */
.pear-compare-bar {
  display: flex; align-items: center; gap: 9px; width: fit-content; margin: 16px auto 2px;
  padding: 12px 22px; border-radius: 50px;
  background: linear-gradient(135deg, var(--pear-tint), rgba(141,182,0,.08));
  -webkit-backdrop-filter: blur(25px) saturate(200%); backdrop-filter: blur(25px) saturate(200%);
  border: 1px solid rgba(141,182,0,.45);
  color: var(--pear-deep); font-family: "Urbanist", sans-serif; font-weight: 800; font-size: .92rem;
  cursor: pointer; box-shadow: 0 10px 30px rgba(141,182,0,.22), inset 0 1px 0 rgba(255,255,255,.55);
  transition: all .5s cubic-bezier(.16, 1, .3, 1);
  animation: compareBarIn .5s cubic-bezier(.16, 1, .3, 1) both;
}
/* Unlike .cam-controls/.add-cart-row above, this pill is width:fit-content
   with margin:auto on BOTH sides - true centering, not a full-width row a
   padding-inline-start could shrink-and-shift. It also lives mid-gallery, not
   pinned to the top or bottom of the page, so as the page scrolls it - like
   any other mid-page content - passes through the fixed help button's
   permanent y-band at some scroll position on every visit that reaches it, not
   just some: measured up to 138 of 138 sampled points inside the overlap
   resolving to the help button, a real, complete win for the wrong control.
   Swapping the trailing auto margin for a fixed one keeps this pinned exactly
   --help-fab-gutter clear of the button's side at every scroll position,
   trading perfect centering (this is a secondary "Compare Looks" pill, not the
   screen's primary action) for a guarantee that holds regardless of scroll. */
@media (max-width: 600px) {
  .pear-compare-bar { margin-inline-start: var(--help-fab-gutter); margin-inline-end: auto; }
}
.pear-compare-bar[hidden] { display: none; }
.pear-compare-bar:hover { transform: translateY(-2px) scale(1.02); box-shadow: 0 16px 38px rgba(141,182,0,.3); }
.pear-compare-bar:active { transform: scale(.96); }
.pear-compare-bar__icon { font-size: 1.05rem; }
.pear-compare-bar__en { font-size: .7rem; font-weight: 600; opacity: .6; letter-spacing: .04em; }
@keyframes compareBarIn { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }

/* ── Compare Fits split-screen liquid-glass overlay ── */
.pear-compare-overlay {
  position: fixed; inset: 0; z-index: 220; display: none;
  align-items: center; justify-content: center; padding: clamp(16px, 4vw, 40px);
}
.pear-compare-overlay[hidden] { display: none; }
.pear-compare-overlay.show { display: flex; animation: fade .4s var(--ease) both; }
.pcmp__backdrop {
  position: absolute; inset: 0; background: rgba(8,8,10,.55);
  -webkit-backdrop-filter: blur(25px) saturate(200%); backdrop-filter: blur(25px) saturate(200%);
}
.pcmp__panel {
  position: relative; z-index: 1; width: min(960px, 96vw); max-height: 92vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 16px; padding: clamp(18px, 3vw, 26px);
  background: linear-gradient(135deg, rgba(255,255,255,.4), rgba(255,255,255,.14));
  -webkit-backdrop-filter: blur(25px) saturate(200%); backdrop-filter: blur(25px) saturate(200%);
  border: 1px solid rgba(255,255,255,.4); border-radius: 28px;
  box-shadow: var(--shadow-lift), inset 0 1px 0 rgba(255,255,255,.5);
  animation: galleryItemIn .45s cubic-bezier(.25, 1, .5, 1) both;
}
/* heading settles a beat after .pcmp__panel itself, echoing the same
   card-then-heading ordering used on screen swap above */
.pcmp__head { display: flex; align-items: center; gap: 12px; animation: fade .4s var(--ease) both .06s; }
.pcmp__title { margin: 0; font-size: 1.1rem; font-weight: 800; }
.pcmp__title span:not([data-i18n]) { color: var(--pear-deep); font-weight: 700; font-size: .85rem; }
.pcmp__close {
  margin-inline-start: auto; width: 38px; height: 38px; border-radius: 50%;
  border: 1px solid var(--glass-border); background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--ink); font-size: 1.05rem; line-height: 1; cursor: pointer;
  transition: all .5s cubic-bezier(.16, 1, .3, 1);
}
.pcmp__close:hover { transform: scale(1.08) rotate(90deg); background: #fff; }
.pcmp__close:active { transform: scale(.94); }
.pcmp__split { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; min-height: 0; }
.pcmp__cell {
  display: flex; flex-direction: column;
  border: 1px solid var(--glass-border); border-radius: 20px; overflow: hidden;
  background: var(--glass);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  box-shadow: var(--shadow-soft);
  animation: galleryItemIn .5s cubic-bezier(.25, 1, .5, 1) both;
}
.pcmp__cell:nth-child(2) { animation-delay: .08s; }
.pcmp__media { aspect-ratio: 3 / 4; background: #050505; overflow: hidden; }
.pcmp__media video, .pcmp__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pcmp__label { display: flex; align-items: center; gap: 8px; padding: 11px 14px; font-size: .82rem; color: var(--ink); }
.pcmp__label b { font-family: var(--font-ui); font-weight: 800; }
.pcmp__label .pcmp__size { margin-inline-start: auto; color: var(--pear-deep); font-family: var(--font-ui); font-weight: 900; }
@media (max-width: 640px) {
  .pcmp__split { grid-template-columns: 1fr; }
  .pcmp__panel { max-height: 94vh; overflow-y: auto; }
  .pcmp__media { aspect-ratio: 16 / 10; }
}

/* ════════════════════════════════════════════════════════════════════════
   Extra micro-motion - premium entrance + ambient life
   ════════════════════════════════════════════════════════════════════════ */
@keyframes riseIn { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: none; } }

/* calculator card content cascades up on entry */
#screen-calculator.active .kicker          { animation: riseIn .6s var(--ease) both; animation-delay: .06s; }
#screen-calculator.active .container > h2  { animation: riseIn .6s var(--ease) both; animation-delay: .13s; }
#screen-calculator.active .subtitle        { animation: riseIn .6s var(--ease) both; animation-delay: .20s; }
/* Indices count from #sizeForm's FIRST DOM child, #measurementsRefreshNotice (a <p>,
   hidden by default but still a sibling for :nth-child purposes) - so the first
   .form-group (gender) lands on :nth-child(2), not (1). Gender/height/weight, in that
   DOM order. */
#screen-calculator.active #sizeForm > .form-group:nth-child(2) { animation: riseIn .6s var(--ease) both; animation-delay: .27s; }
#screen-calculator.active #sizeForm > .form-group:nth-child(3) { animation: riseIn .6s var(--ease) both; animation-delay: .34s; }
#screen-calculator.active #sizeForm > .form-group:nth-child(4) { animation: riseIn .6s var(--ease) both; animation-delay: .41s; }

/* editorial copy fades up in sequence */
#screen-calculator.active .calc-editorial__kicker   { animation: riseIn .7s var(--ease) both; animation-delay: .10s; }
#screen-calculator.active .calc-editorial__headline { animation: riseIn .7s var(--ease) both; animation-delay: .20s; }
#screen-calculator.active .calc-editorial__lead     { animation: riseIn .7s var(--ease) both; animation-delay: .30s; }
#screen-calculator.active .calc-editorial__list     { animation: riseIn .7s var(--ease) both; animation-delay: .40s; }
#screen-calculator.active .calc-editorial__meta     { animation: riseIn .7s var(--ease) both; animation-delay: .50s; }

/* specular sheen sweeps across primary glass buttons on hover */
/* BUTTON CORNER-CLIP FIX: overflow:hidden alone doesn't reliably antialias a
   border-radius:9999px capsule once a transformed child (the ::after sweep
   below) and backdrop-filter both force their own GPU compositing layer -
   Chromium was rendering the left corners visibly jagged/uneven vs. the
   right. clip-path uses a proper compositing mask instead of a scissor
   clip, so the pill stays crisp on all four corners regardless of what's
   compositing inside it. overflow:hidden stays as a fallback for browsers
   without clip-path support. */
.btn-primary, .btn-capture, .btn-watch {
  position: relative; overflow: hidden;
  -webkit-clip-path: inset(0 round 9999px);
          clip-path: inset(0 round 9999px);
}
.btn-primary::after, .btn-capture::after, .btn-watch::after {
  content: ""; position: absolute; top: 0; left: -130%;
  width: 55%; height: 100%; pointer-events: none;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,.6), transparent);
  transform: skewX(-18deg);
}
.btn-primary:hover:not(:disabled)::after,
.btn-capture:hover:not(:disabled)::after,
.btn-watch:hover::after { animation: btnShine .9s var(--ease); }
@keyframes btnShine { from { left: -130%; } to { left: 140%; } }

/* progress bar - a travelling light sheen rides the fill */
.step-progress-fill { position: relative; overflow: hidden; }
.step-progress-fill::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent, rgba(255,255,255,.55), transparent);
  transform: translateX(-100%); animation: progShine 2.4s var(--ease) infinite;
}
@keyframes progShine { from { transform: translateX(-100%); } to { transform: translateX(240%); } }

/* ── short viewports: keep the whole size card on one screen ────────────── */
@media (max-height: 760px) {
  .step-progress { padding-top: 16px; }
  .calc-main .step-progress { padding-top: 6px; }
  #screen-calculator .container { margin: 8px auto; padding: clamp(16px, 3vw, 26px); }
  .container h2 { font-size: 1.34rem; }
  .subtitle { margin-bottom: 12px; }
  #sizeForm { gap: 10px; }
  .form-group input { padding: 11px 15px; }
  .btn-primary { margin-top: 16px; padding: 13px 24px; }
  .result-box { padding: 14px 18px; }
}

/* ── accessibility: respect reduced-motion ─────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  body::before, body::after { animation: none; }
}

/* ════════════════════════════════════════════════════════════════════════
   ✦ MOBILE EXPERIENCE LAYER - app-like phone UI + orchestrated motion
   ─────────────────────────────────────────────────────────────────────────
   Additive only. Extends the Monochrome Liquid-Glass identity for touch:
   an immersive camera stage, a sticky/shrinking header, a swipeable catalog,
   and a layer of tactile motion (scroll-reveal, ripple, spring presses, live
   aura). Every existing id/class/state hook is preserved - JS logic untouched.
   ════════════════════════════════════════════════════════════════════════ */

:root {
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-l: env(safe-area-inset-left, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --spring: cubic-bezier(.34, 1.56, .64, 1);   /* tactile overshoot */
}

/* ════════════════════════════════════════════════════════════════════════
   ✦ TOP-CORNER CONTROL GEOMETRY - the single source of truth
   ─────────────────────────────────────────────────────────────────────────
   OVERLAP ROOT CAUSE (fixed here): .lang-switch (physical top-LEFT) and
   .profile-btn (physical top-RIGHT) are position:fixed VIEWPORT chrome that
   lives outside .app-header in the DOM - .lang-switch is a direct child of
   <body> (it must survive screen 1, which has no header at all), .profile-btn
   sits just before the header inside #screen-fitting. The header meanwhile
   lays its OWN controls out with justify-content:space-between, so its two end
   clusters run straight into those same two corners. Two independent layout
   systems both claiming the same pixels = guaranteed collision, and because
   the fixed pair carries z-index:100 against the header's 40, they won the
   stack and swallowed the taps: on every phone width the language switch sat
   directly on top of #cartBtn and ate its clicks outright.
   Neither control can simply be moved into the header's flex row (the header
   does not exist on screen 1, and .lang-switch has to be reachable there), so
   the fix is to make the header RESERVE the exact width those corners occupy
   and lay its own row out inside what's left - real flex placement, no
   geometric guesswork, no overlap possible at any width.
   Every number the reservation depends on is a token here, and the fixed
   controls consume the SAME tokens for their size/offset (see .lang-switch /
   .profile-btn below), so the gutter and the thing it clears can never drift
   apart the way two hand-tuned magic numbers eventually do.
   ════════════════════════════════════════════════════════════════════════ */
:root {
  /* ── ONE HEIGHT, ONE TOP EDGE, FOR EVERY HEADER CONTROL ──────────────────
     THE REPORTED PROBLEM: the top-bar controls sat at visibly different
     heights and did not share a baseline. Measured, they were five different
     boxes with five different vertical centres:

       control                  height   top edge   centre
       .lang-switch              38px    16px       35px
       .fs-switch                36px    24px       42px
       .btn-edit-measurements    27px    24px      ~37px
       .cart-btn                 42px    24px       45px
       .profile-btn              40px    16px       36px

     Two independent causes, and both had to be fixed or the row would still
     be ragged. (1) Every control sized itself from its own content - padding
     plus a label, or a hand-set 42px square - so no two agreed. (2)
     .lang-switch and .profile-btn are position:absolute corner controls, NOT
     flex children of .app-header, so they took their vertical origin from
     --corner-inset (16px) while the row took its own from the header's
     padding-top (24px on desktop, 8px on phones - where the same pair then
     sat 8px BELOW the row instead of above it, the same bug with the sign
     flipped).

     So the fix is two tokens rather than a set of nudges. Every control now
     resolves its height from --hdr-control-h, and everything that establishes
     a top edge - the header's own padding and both absolute corner controls -
     resolves it from --hdr-pad-t. Equal heights plus equal top edges is equal
     centres, by construction, at every breakpoint, with no per-control
     tuning to drift.

     WHY A HEIGHT TOKEN AND NOT align-items:center: the row already had
     align-items:center, and it was doing its job - centring five boxes of
     five different heights, which is exactly what it is asked to do and is
     still a ragged row. Alignment cannot fix a height disagreement.

     --corner-inset stays, unchanged, as the HORIZONTAL inset: it is what
     --hdr-gutter-l/r reserve against, and that reservation is a different
     measurement from where the controls' top edge sits. Splitting them is
     what lets the vertical origin move without touching the collision math. */
  --hdr-control-h: 40px;   /* shared height of EVERY header control */
  --hdr-pad-t:     24px;   /* shared top edge: header padding AND both corner controls */

  /* ── THE ROW HEIGHT HAD TO BECOME A NUMBER TOO ───────────────────────────
     Equal control heights and equal top edges were necessary and not
     sufficient, because the ROW is not as tall as the controls in it. The
     brand lockup is the tallest zone and its height comes from type metrics:

       .app-header__brand  1.4rem, line-height 1    = 22.40px
       .app-header__titles gap                      =  3.00px
       .app-header__sub    .84rem, line-height 1.2  = 16.13px
                                                      ------
                                                      41.53px   (> the 38px logo)

     So the row resolved to 41.53px. Its flex children centre on THAT - at
     padding-top + 20.76 - while the two absolute corner controls, anchored to
     the padding edge with a height of 40, centre at padding-top + 20. A 0.76px
     disagreement: small, visible against a circle's edge, and impossible to
     nudge away permanently because the number is a font measurement. It moves
     with every type change and differs at each breakpoint (the wordmark is
     1.4rem here, 1.2rem when the phone header compacts).

     --hdr-row-h fixes the row at a round number the lockup fits inside, so the
     centreline stops being derived from font metrics at all. The corner pair
     then anchors to that CENTRE - (row - control) / 2 below the padding edge -
     rather than to the padding edge itself.

     min-height, not height, on the zones: at the nominal type size the lockup
     (41.53px) is comfortably inside 44px so the row IS exactly 44, while a
     shopper who zooms text still gets a row that grows instead of clipping.
     Under that growth the corner pair drifts again by half the overflow - the
     correct trade, since a clipped wordmark is worse than a 1px offset at
     200% zoom. */
  --hdr-row-h:     44px;   /* deterministic row height - NOT the type's 41.53px */

  --corner-inset:  16px;   /* physical HORIZONTAL inset of both fixed corner controls */
  --corner-clear:  10px;   /* deliberate breathing gap before header content */
  --profile-size:  var(--hdr-control-h);   /* .profile-btn circle - was a separate 40px */
  --lang-seg-w:    42px;   /* .lang-switch__seg / __thumb - MIRRORED in i18n.js */
  --fs-seg-w:      34px;   /* .fs-switch__seg  / __thumb - MIRRORED in app.js  */
  --switch-pad:     4px;   /* segmented-switch track padding */

  /* Overall track width of a 2-segment glass switch: both segments + the
     track's own padding + its 1px specular border on each side. */
  --lang-switch-w: calc(2 * var(--lang-seg-w) + 2 * var(--switch-pad) + 2px);

  /* What the header must keep clear on each PHYSICAL edge. Physical, not
     logical: the fixed pair is pinned with left/right and never mirrors with
     dir, so these must not mirror either. */
  --hdr-gutter-l: calc(var(--safe-l) + var(--corner-inset) + var(--lang-switch-w) + var(--corner-clear));
  --hdr-gutter-r: calc(var(--safe-r) + var(--corner-inset) + var(--profile-size)  + var(--corner-clear));
}

/* ── scroll-reveal (mobile-ux.js adds .reveal-up, then .in-view) ──────────
   The hidden state only exists once JS has tagged the element, so a no-JS or
   reduced-motion visit renders every block fully visible. */
.reveal-up {
  opacity: 0; transform: translateY(26px) scale(.99);
  transition: opacity .7s var(--ease), transform .8s var(--ease);
  will-change: opacity, transform;
}
.reveal-up.in-view { opacity: 1; transform: none; }

/* ── tap ripple (mobile-ux.js spawns .pear-ripple inside the pressed control) ── */
.btn-capture, .btn-primary, .btn-watch, .btn-download,
.pip-retake, .plb-btn, .pear-compare-bar { position: relative; overflow: hidden; }
.btn-capture > *, .btn-primary > *, .btn-watch > *, .btn-download > *,
.pip-retake > *, .plb-btn > *, .pear-compare-bar > * { position: relative; z-index: 1; }
.pear-ripple {
  position: absolute; z-index: 0; border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, rgba(141,182,0,.55) 0%, rgba(255,255,255,.38) 42%, transparent 70%);
  transform: scale(0); opacity: .85; mix-blend-mode: screen;
  animation: pearRipple .62s var(--ease) forwards;
}
@keyframes pearRipple { to { transform: scale(2.5); opacity: 0; } }

/* sticky header animates its compaction on every viewport */
.app-header {
  transition: padding .45s var(--ease), background .45s var(--ease),
              box-shadow .45s var(--ease), border-color .45s var(--ease);
}

/* ════════════════════════════════════════════════════════════════════════
   PHONE  (≤ 768px)
   ════════════════════════════════════════════════════════════════════════ */
@media (max-width: 768px) {

  /* sticky, frosted app chrome that compacts as you scroll into the stage.
     DEAD-SPACE FIX: top/bottom padding trimmed (was 14px/14px, scrolled 8px/8px) -
     the row's own content height was already carrying the visual weight; this was
     pure extra air above/below it. */
  /* The shared top edge is tighter on phones. Declared on :root rather than on
     .app-header, because the two absolute corner controls are NOT descendants of
     the header and would not see it there - which is exactly how they came to sit
     8px BELOW the row here while sitting 8px above it on desktop. */
  :root { --hdr-pad-t: 8px; }
  .app-header {
    position: sticky; top: 0; z-index: 40;
    max-width: none; margin: 0;
    --hdr-pad-x: clamp(16px, 5vw, 22px);
    padding: calc(var(--hdr-pad-t) + var(--safe-t)) var(--hdr-pad-x) var(--hdr-pad-t);
    /* Full-bleed here (max-width:none), so the box edge IS the viewport edge -
       the centred-box term in the base rule collapses to zero and the corner
       gutters apply at full value. Stated directly rather than left to the
       base rule's max(), so the reservation is obvious at the breakpoint that
       actually needs it. */
    padding-left: var(--hdr-gutter-l);
    padding-right: var(--hdr-gutter-r);
    gap: 8px;   /* was the base rule's 12px - tighter, more compact on phones */
    background: linear-gradient(180deg, rgba(247,247,246,.88), rgba(247,247,246,.30));
    -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
    border-bottom: 1px solid transparent;
  }
  /* Compaction stays scoped to the header and does NOT move --hdr-pad-t: by the
     time this class exists the two corner controls have scrolled off the top of
     the viewport (they are position:absolute, not fixed - see body.corners-cleared),
     so there is nothing left to keep aligned and re-anchoring them would only
     animate two off-screen boxes. */
  .app-header.is-scrolled {
    padding-top: calc(5px + var(--safe-t)); padding-bottom: 5px;
    border-bottom-color: var(--glass-border);
    box-shadow: 0 6px 20px rgba(0, 0, 0, .06);
  }
  /* REVERSED: .focus-bar is no longer sticky (see its own rule), so there is
     nothing left for the header to cede its slot to - .app-header now stays
     sticky unconditionally, focus mode or not, matching every other screen. */
  /* PROMINENCE FIX: was 1.26rem/1.1rem (scrolled) - scaled up so the wordmark reads
     as the header's visual anchor instead of getting lost in the trimmed padding. */
  .app-header__brand { font-size: 1.4rem; transition: font-size .45s var(--ease); }
  .app-header.is-scrolled .app-header__brand { font-size: 1.2rem; }
  .app-header__sub { font-size: .78rem; }

  /* roomier, thumb-first body; keep the live control clear of the home indicator */
  .fitting-container {
    padding: 18px clamp(14px, 4.5vw, 22px) calc(36px + var(--safe-b));
  }
  .personal-title { font-size: 1.5rem; line-height: 1.18; margin: 6px 0 20px; }

  /* ── immersive camera stage - near-full-bleed, deeper, alive ── */
  .camera-card {
    max-width: 100%;
    border-radius: 30px;
    box-shadow: 0 20px 54px rgba(0, 0, 0, .18), inset 0 0 0 1px rgba(255, 255, 255, .08);
  }
  /* a soft pear breath while idle (before the camera opens) so the stage never feels dead */
  .camera-card:not(.live):not(.show-live):not(.show-result) {
    animation: stageIdle 5.5s var(--ease) infinite;
  }
  @keyframes stageIdle {
    0%, 100% { box-shadow: 0 20px 54px rgba(0,0,0,.18), inset 0 0 0 1px rgba(255,255,255,.08); }
    50%      { box-shadow: 0 24px 64px rgba(0,0,0,.20), 0 0 30px rgba(141,182,0,.16),
                           inset 0 0 0 1px rgba(141,182,0,.20); }
  }
  .camera-card__placeholder p { font-size: .96rem; }
  .camera-card__icon { font-size: 3rem; }

  /* ── big, tactile primary control ── */
  .cam-controls { margin-top: 18px; gap: 12px; }
  .btn-capture {
    padding: 18px 24px; border-radius: 26px; min-height: 64px;
    /* clip-path kept in sync with the border-radius override above (see the
       corner-clip fix note near .btn-primary/.btn-capture/.btn-watch) */
    -webkit-clip-path: inset(0 round 26px);
            clip-path: inset(0 round 26px);
    transition: transform .35s var(--spring), background .4s var(--ease),
                border-color .4s var(--ease), box-shadow .4s var(--ease);
  }
  .btn-capture__icon  { font-size: 1.35rem; }
  .btn-capture__label { font-size: 1.16rem; }
  .btn-capture:active:not(:disabled) { transform: scale(.945); }
  /* a clearer "ready" invite pulse on the phone */
  .btn-capture:not(:disabled):not(.is-live) { animation: capInvite 2.4s ease-in-out infinite; }
  @keyframes capInvite {
    0%, 100% { box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,.55); }
    50%      { box-shadow: 0 14px 34px rgba(141,182,0,.30), 0 0 0 5px rgba(141,182,0,.10),
                           inset 0 1px 0 rgba(255,255,255,.6); }
  }

  /* ── catalog → swipeable snap carousel (peek the next card) ── */
  .catalog-panel { margin-top: 32px; }
  .catalog-grid {
    display: flex; grid-template-columns: none; gap: 12px;
    overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
    scrollbar-width: none; padding-bottom: 8px; scroll-padding-inline: 2px;
  }
  .catalog-grid::-webkit-scrollbar { width: 0; height: 0; display: none; }
  .cat-item { flex: 0 0 44vw; max-width: 172px; scroll-snap-align: start; }
  .cat-item:active { transform: scale(.95); }

  /* spacing rhythm for the remaining sections */
  .complete-look { margin-top: 34px; }
  .active-garment { padding: 13px; border-radius: var(--radius); margin-bottom: 18px; }
  .pear-interaction-pod { margin-top: 32px; border-radius: 26px; }

  /* lift the toast above the home indicator */
  .toast { bottom: calc(24px + var(--safe-b)); }
}

/* ════════════════════════════════════════════════════════════════════════
   SMALL PHONE  (≤ 480px) - declutter + tune
   ════════════════════════════════════════════════════════════════════════ */
@media (max-width: 480px) {
  .personal-title { font-size: 1.34rem; }
  .app-header__sub { display: none; }                  /* brand + engine dot is enough */
  .camera-card { border-radius: 26px; }
  .btn-capture { min-height: 60px; }
  .cat-item { flex-basis: 52vw; }
  .live-countdown { width: 58px; height: 58px; }
  .live-countdown__num { font-size: 1.34rem; }
}

/* ════════════════════════════════════════════════════════════════════════
   ✦ LUXURY MICRO-INTERACTION LAYER  (iOS / macOS-grade polish)
   ─────────────────────────────────────────────────────────────────────────
   Paired with lux-interactions.js. Add-to-Cart fly · Bitten-Pear transition ·
   ambient side rails · magnetic buttons · floating labels · spring cart toast ·
   metallic skeleton. Pear-green accent, Monochrome Liquid-Glass language.
   ════════════════════════════════════════════════════════════════════════ */
:root { --power: cubic-bezier(0.25, 1, 0.5, 1); }   /* Apple signature power-ease */

/* ── 1 · Header cart icon ──────────────────────────────────────────────── */
.cart-btn {
  position: relative; align-self: center; margin-inline-start: auto;
  display: inline-flex; align-items: center; justify-content: center;
  /* Was a hand-set 42px square - the tallest control in the row, and the one
     every other control was visually measured against. Now the shared height,
     which also makes it exactly the same circle as .profile-btn opposite it. */
  width: var(--hdr-control-h); height: var(--hdr-control-h); border-radius: 50%;
  border: 1px solid var(--glass-border);
  background: var(--glass-strong);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--ink); cursor: pointer;
  box-shadow: var(--shadow-soft), inset 0 1px 0 rgba(255,255,255,.6);
  transition: transform .5s var(--power), box-shadow .5s var(--power), background .35s var(--ease);
}
.cart-btn:hover  { transform: translateY(-2px); background: rgba(255,255,255,.85); box-shadow: var(--shadow); }
.cart-btn:active { transform: scale(.92); }
/* iOS-style jiggle when an item lands */
.cart-btn.is-bounce { animation: cartJiggle .68s var(--power); }
@keyframes cartJiggle {
  0%   { transform: scale(1)    rotate(0); }
  22%  { transform: scale(1.22) rotate(-8deg); }
  44%  { transform: scale(.92)  rotate(6deg); }
  64%  { transform: scale(1.08) rotate(-4deg); }
  82%  { transform: scale(.98)  rotate(2deg); }
  100% { transform: scale(1)    rotate(0); }
}
.cart-btn__count {
  position: absolute; top: -4px; inset-inline-end: -4px;
  min-width: 19px; height: 19px; padding: 0 5px; border-radius: var(--pill);
  display: inline-flex; align-items: center; justify-content: center;
  background: linear-gradient(135deg, var(--pear), var(--pear-deep));
  color: #fff; font-family: var(--font-ui); font-weight: 900; font-size: .68rem;
  line-height: 1; border: 1.5px solid rgba(255,255,255,.9);
  box-shadow: 0 3px 10px rgba(141,182,0,.4);
  transition: transform .5s var(--spring), opacity .35s var(--ease);
}
.cart-btn__count.is-empty { transform: scale(0); opacity: 0; }

/* ── Cart dropdown - mirrors the HOST store's real cart (Full Bi-directional
   Cart Sync: app.js <-> pear-widget.js). Anchored to .app-header__actions
   (position:relative, set where that zone is defined further down) rather
   than fixed like .profile-dropdown - #cartBtn sits in normal document flow,
   not a fixed viewport corner, so a hardcoded fixed offset would drift out
   from under it the moment the header's own layout/scroll state changes. ── */
.cart-dropdown {
  position: absolute; top: calc(100% + 8px); inset-inline-end: 0; z-index: 9999;
  /* BUG FIX: was a hardcoded `direction: rtl`, which left the popover
     right-to-left (text alignment, and the remove button's side) even in
     English. Inherited from <html dir>, which i18n.js already keeps current,
     so the popover follows the active language like the rest of the UI. */
  direction: inherit;
  /* OVERFLOW FIX: was a flat 280px with max-width:calc(100vw - 32px), which
     measures the VIEWPORT but ignores where this popover is actually anchored -
     inset-inline-end:0 on .app-header__actions, i.e. offset from the viewport
     edge by whatever gutter the header reserves for the fixed corner controls.
     At 320px that put 48px of the panel off-screen. The cap now subtracts the
     LARGER of the two gutters so it holds in both directions (RTL anchors the
     panel at the physical-left gutter, LTR at the physical-right one), and is
     a no-op at every width where 280px already fits. */
  width: min(280px, calc(100vw - max(var(--hdr-gutter-l), var(--hdr-gutter-r)) - 12px));
  max-height: 60vh; overflow-y: auto;
  padding: 14px;
  border: 1px solid var(--glass-border); border-radius: 16px;
  /* Explicitly opaque enough that NOTHING behind it (the page h1/h2, the
     camera stage) can read through - backdrop-filter alone is not a
     guarantee, since a browser that ignores/degrades it would otherwise
     leave this popover translucent over live content. */
  background: rgba(255, 255, 255, .97);
  -webkit-backdrop-filter: blur(20px) saturate(180%); backdrop-filter: blur(20px) saturate(180%);
  box-shadow: 0 12px 32px rgba(0, 0, 0, .18), 0 2px 8px rgba(0, 0, 0, .08);
  animation: fade .25s var(--ease) both;
}
.cart-dropdown[hidden] { display: none; }

/* BUG FIX (popover rendering UNDER page chrome): on phones .app-header is
   `position: sticky; z-index: 40`, which makes it a stacking context - so the
   popover's own z-index could only ever compete INSIDE it, and fixed siblings
   later in the document (.lang-switch / .profile-btn at 100, the help widget
   at 150) painted straight over it no matter how high that z-index went.
   Raising the header's own context while the popover is open is what actually
   lifts it clear. 190 is deliberate: above ordinary page chrome (help widget
   150) but BELOW every real overlay - lightbox 200, compare 220, toast 300,
   garment-detect 400 - so an open popover can never float over a modal.
   Toggled from setupCartButton() in app.js, only for as long as it's open. */
.app-header.has-cart-open { position: relative; z-index: 190; }

.cart-dropdown__head {
  display: flex; align-items: baseline; gap: 6px;
  font-family: var(--font-ui); font-size: .95rem; color: var(--ink);
  padding-bottom: 10px; margin-bottom: 10px; border-bottom: 1px solid var(--glass-line);
}
/* the heading itself may shrink/ellipsize; the count chip never does */
.cart-dropdown__head strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cart-dropdown__count {
  flex: none; margin-inline-start: auto;
  font-family: var(--font-ui); font-size: .76rem; font-weight: 700; color: var(--muted);
}
.cart-dropdown__count:empty { display: none; }

.cart-dropdown__empty {
  margin: 6px 0 2px; text-align: center; font-size: .84rem; color: var(--muted);
  line-height: 1.45;
}
.cart-dropdown__empty[hidden] { display: none; }

.cart-dropdown__list { display: flex; flex-direction: column; gap: 10px; }

.cart-line {
  position: relative; display: flex; align-items: center; gap: 10px;
  padding-inline-end: 30px;   /* room for the absolutely-positioned remove button */
}
.cart-line__media {
  width: 46px; height: 46px; flex: none; border-radius: 10px; overflow: hidden;
  background: rgba(0, 0, 0, .05); border: 1px solid var(--glass-line);
}
.cart-line__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cart-line__meta { min-width: 0; flex: 1 1 auto; }
.cart-line__title {
  display: block; font-size: .84rem; font-weight: 700; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cart-line__variant {
  display: block; font-size: .74rem; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.cart-line__qty { font-size: .74rem; color: var(--muted); }
.cart-line__remove {
  position: absolute; top: 0; inset-inline-end: 0;
  width: 24px; height: 24px; padding: 0; border: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, .05); color: var(--muted);
  font-size: .78rem; line-height: 1; cursor: pointer;
  transition: background .25s var(--ease), color .25s var(--ease);
}
.cart-line__remove:hover { background: rgba(220, 60, 60, .16); color: #b23a3a; }
.cart-line.is-removing { opacity: .4; pointer-events: none; }   /* optimistic remove, pending host confirmation */

/* ── Profile button (Feature 3) - fixed top-right corner, always visible in the
   fitting room once a user is known. Same frosted-circle language as .cart-btn,
   pear-green accented. z-index sits above the camera stage (highest in-camera
   z-index is 9, see .camera-card__* rules) but below any modal/overlay
   (garment-detect / lightbox / compare start at 200+). ── */
.profile-btn {
  /* ABSOLUTE, matching .lang-switch: both top-corner controls belong to the
     TOP OF THE PAGE and scroll away with it, while .app-header keeps sticking.
     This was already the OBSERVED behaviour, but only by accident, and only in
     some browsers: it is declared inside #screen-fitting, whose .screen.active
     `fade` animation ends on `transform: none` under animation-fill-mode:both.
     Chrome resolves that filled value to an identity matrix, which is enough to
     make the screen a containing block and quietly demote this `fixed` to
     absolute - which is why the avatar scrolls off. A browser that resolves the
     same keyframe to a genuine `none` creates no containing block, leaving the
     avatar truly viewport-fixed and pinned on screen while everything else
     scrolls. Declaring `absolute` outright makes the intended behaviour the
     same everywhere instead of depending on how a keyframe happens to compute.
     body is position:relative with margin:0, so the resolved position is
     identical to what the trapped-fixed version produced. */
  /* --safe-t for the same reason as .lang-switch above (viewport-fit=cover).
     It also removes a jump this button used to make on notched phones: the
     base offset had no --safe-t while the body.is-scrolled override below
     does, so crossing the 12px scroll threshold shifted it down by the whole
     inset - ~45px on an iPhone. Both ends now include it, leaving the
     intended 2px of compaction. */
  position: absolute;
  top: calc(var(--safe-t) + var(--hdr-pad-t) + (var(--hdr-row-h) - var(--hdr-control-h)) / 2);
  z-index: 100;
  /* --hdr-pad-t, NOT --corner-inset: this control is absolutely positioned and
     is therefore the one thing in the header that cannot inherit the row's top
     edge. Sharing the token is what keeps it on the row's baseline at every
     breakpoint - it used to sit 8px above the row on desktop and 8px below it
     on phones. --corner-inset still owns the horizontal inset below. */
  /* Physical right + the landscape safe-area inset, matching --hdr-gutter-r
     exactly (see the TOP-CORNER CONTROL GEOMETRY block). */
  right: calc(var(--safe-r) + var(--corner-inset));
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--profile-size); height: var(--profile-size); border-radius: 50%; padding: 0;
  border: 1px solid rgba(255, 255, 255, .16);
  background: var(--glass-dark);
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: #fff; cursor: pointer;
  box-shadow: var(--shadow-soft), inset 0 1px 0 rgba(255,255,255,.1);
  transition: box-shadow .3s var(--ease), background .3s var(--ease),
              top .3s var(--ease), width .3s var(--ease), height .3s var(--ease),
              /* Asymmetric like the focus-bar padding above: a springy pop
                 OUT of view, a calmer ease back IN - set on the base rule so
                 it's the "returning to visible" direction's timing; the
                 disappear rule below overrides both with its own pop. */
              transform .35s var(--ease), opacity .35s var(--ease);
}
.profile-btn:hover  { transform: translateY(-2px); background: rgba(16, 16, 18, .8); box-shadow: var(--shadow); }
.profile-btn:active { transform: scale(.92); }
.profile-btn[hidden] { display: none; }

/* ── Language segmented switch - fixed top-left corner (mirrors .profile-btn's
   top-right). Apple Liquid Glass drag physics - the SAME component
   architecture as .fs-switch (the fullscreen toggle): a translucent sliding
   thumb behind two segments, driven by setupLangToggle() in i18n.js
   (pointerdown/move/up for a plain click OR a real press-and-drag, both
   resolving through the same code path). direction:ltr locks this control's
   OWN internal layout to a fixed physical order (EN always left, עב always
   right) regardless of which language is currently active - see the comment
   on setupLangToggle() in i18n.js for why that's the right call here. */
.lang-switch {
  /* ABSOLUTE, not fixed - by request: this control belongs to the TOP OF THE
     PAGE, not to the viewport. It sits in the header row at rest and then
     scrolls away with the document, while everything else in .app-header keeps
     sticking. body is position:relative with no transform/filter, so this
     resolves against the document origin and simply scrolls off the top.
     z-index stays above .app-header (40) so it passes cleanly OVER the frosted
     bar on its way out instead of dissolving through the blur.
     Once it has fully cleared the viewport, mobile-ux.js flags
     body.lang-cleared and the header reclaims the gutter reserved for it -
     see the .lang-switch reservation notes on .app-header. */
  /* --safe-t, like .app-header's own padding-top: the document sets
     viewport-fit=cover, so without it this sits UNDER the notch / status bar
     on every notched phone. */
  position: absolute;
  top: calc(var(--safe-t) + var(--hdr-pad-t) + (var(--hdr-row-h) - var(--hdr-control-h)) / 2);
  z-index: 100;
  /* --hdr-pad-t, NOT --corner-inset: this control is absolutely positioned and
     is therefore the one thing in the header that cannot inherit the row's top
     edge. Sharing the token is what keeps it on the row's baseline at every
     breakpoint - it used to sit 8px above the row on desktop and 8px below it
     on phones. --corner-inset still owns the horizontal inset below. */
  /* Physical left + the landscape safe-area inset, matching --hdr-gutter-l
     exactly (see the TOP-CORNER CONTROL GEOMETRY block). */
  left: calc(var(--safe-l) + var(--corner-inset));
  direction: ltr;
  display: inline-flex; align-items: center;
  padding: var(--switch-pad); margin: 0; border: 0;
  appearance: none; -webkit-appearance: none;
  border-radius: var(--pill);
  background: var(--glass-soft);
  -webkit-backdrop-filter: blur(16px) saturate(180%); backdrop-filter: blur(16px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, .4);   /* specular border, per the Liquid Glass spec */
  box-shadow: var(--shadow-soft);
  cursor: pointer; touch-action: none;   /* pointer drag owns touch gestures here, not page scroll */
}
.lang-switch:hover { box-shadow: 0 8px 22px rgba(0, 0, 0, .12); }
.lang-switch:active { cursor: grabbing; }
.lang-switch:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--pear-ring), var(--shadow-soft); }

/* The translucent pill indicator - slides between the EN/עב slots via
   --thumb-x (set inline by i18n.js, live during drag; snapped to the exact
   segment width on release/click). Ambient soft drop-shadow + specular
   border, both per the Liquid Glass spec. */
.lang-switch__thumb {
  position: absolute; top: var(--switch-pad); left: var(--switch-pad); z-index: 0;
  /* Width is authored to EQUAL one segment - i18n.js measures this node to get
     the live segment width, so the drag/snap math follows any responsive
     resize automatically instead of trusting a hardcoded constant. */
  width: var(--lang-seg-w); height: calc(100% - 2 * var(--switch-pad));
  border-radius: var(--pill);
  background: rgba(255, 255, 255, .82);
  border: 1px solid rgba(255, 255, 255, .4);
  box-shadow: 0 3px 10px rgba(0, 0, 0, .18), inset 0 1px 0 rgba(255, 255, 255, .6);
  transform: translateX(var(--thumb-x, 0px));
  transition: transform .5s var(--spring);   /* the release "snap" */
  pointer-events: none;   /* the track (.lang-switch itself) owns all pointer handling */
}
/* live-drag: no transition lag while the thumb is tracking the pointer 1:1 -
   the spring above only plays on the SNAP once the pointer is released. */
.lang-switch.is-dragging .lang-switch__thumb { transition: none; }

.lang-switch__seg {
  position: relative; z-index: 1;   /* above the absolutely-positioned thumb */
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--lang-seg-w); height: 28px;
  font-size: .74rem; font-weight: 700; color: var(--muted);
  transition: color .3s var(--ease);
}
.lang-switch__seg.is-active { color: var(--ink); }

@media (prefers-reduced-motion: reduce) {
  .lang-switch__thumb { transition: none; }
}

/* ── Floating help widget (help-widget.js) - fixed bottom corner, Liquid Glass ─
   `inset-inline-start` (not left/right) is the whole RTL/LTR trick: it resolves
   to the LEFT in LTR (English) and the RIGHT in RTL (Hebrew) purely from
   <html dir>, which i18n.js already keeps current - no [dir="rtl"] override
   needed here, matching the inset-inline-* convention used elsewhere in this
   file.

   Visual language: warm-frosted glass (blur+saturate, a cream-white tint, a
   faint white refraction border) matching #screen-calculator .container /
   .kicker rather than the flat dark .glass-dark chip the button used to be -
   and the same olive-green ink (#3d4825) the brand already uses as a true
   fill elsewhere (.pg-swatch.is-active) for the icon, title, list markers and
   the button's own "open" state, instead of generic grayscale UI chrome.
   --help-accent is scoped to this component only - it does not touch the
   site-wide --pear-deep token (near-black by design everywhere else) or any
   of the many components that rely on it staying that way. */
/* SOLID BY DESIGN: this button used to fade out and drop pointer-events
   whenever a primary control (capture / add-to-cart / any .btn-primary)
   scrolled underneath its fixed corner - a guard against the button stealing
   a tap meant for that control. In practice those full-bleed rows pass
   through this exact corner constantly during ordinary scrolling, so the
   button was fading in and out on nearly every scroll gesture - reported as
   "popping on and off, on and off." That trade was the wrong one: a help
   affordance that visibly flickers reads as broken far more than the rare,
   narrow sliver of tap overlap it was preventing. The guard is gone; this
   button is now always visible, always in the same corner, always
   interactive - full stop.
   What still guards against stealing a real tap is `clip-path` below, which
   trims the button's hit area down to its actual visible circle - the
   transparent corners of its square bounding box (the only part that could
   ever land on a control it isn't visually covering) no longer intercept
   anything. */
.pear-help-widget {
  position: fixed; bottom: calc(20px + var(--safe-b)); inset-inline-start: 20px; z-index: 150;
  --help-accent: #3d4825;
}

.pear-help-widget__btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: 50%; padding: 0;
  /* Hit-test the VISIBLE circle only, not its square bounding box - see the
     SOLID BY DESIGN note on .pear-help-widget. A round border-radius:50% button
     still has a full square box of pointer-events by default; without this,
     the box's four transparent corners could each land on whatever sits
     beneath them even though nothing is visually there to tap. */
  clip-path: circle(50%);
  border: 1px solid rgba(255, 255, 255, .4);
  background: linear-gradient(135deg, rgba(255, 253, 246, .92) 0%, rgba(250, 247, 235, .78) 100%);
  -webkit-backdrop-filter: blur(16px) saturate(180%); backdrop-filter: blur(16px) saturate(180%);
  color: var(--help-accent); font-family: "Urbanist", sans-serif; font-weight: 800; font-size: 1.15rem;
  cursor: pointer;
  box-shadow: 0 8px 32px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .6);
  transition: box-shadow .3s var(--ease), background .3s var(--ease),
              border-color .3s var(--ease), transform .25s var(--ease);
}
.pear-help-widget__btn:hover {
  transform: translateY(-2px);
  background: linear-gradient(135deg, rgba(255, 253, 246, .98) 0%, rgba(250, 247, 235, .90) 100%);
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255, 255, 255, .7);
}
.pear-help-widget__btn:active { transform: scale(.92); }
.pear-help-widget__btn:focus-visible {
  outline: none; box-shadow: 0 0 0 4px var(--pear-ring), 0 8px 32px rgba(0, 0, 0, .06);
}
/* "open" state - the same olive tint used for an active swatch/tab elsewhere,
   so the button visibly reads as engaged rather than relying on the popover
   alone to communicate it. */
.pear-help-widget__btn[aria-expanded="true"] {
  background: linear-gradient(135deg, rgba(141, 182, 0, .26) 0%, rgba(141, 182, 0, .14) 100%);
  border-color: rgba(141, 182, 0, .4);
}
.pear-help-widget[hidden] { display: none; }

.pear-help-widget__popover {
  position: absolute; bottom: calc(100% + 12px); inset-inline-start: 0;
  width: min(320px, calc(100vw - 40px));
  /* never grow past the viewport on a short screen - scroll inside instead
     (same guard as .cart-dropdown / .profile-dropdown) */
  max-height: calc(100vh - 120px); overflow-y: auto;
  padding: 18px;
  border: 1px solid rgba(255, 255, 255, .4); border-radius: var(--radius-lg);
  background: linear-gradient(135deg, rgba(255, 253, 246, .96) 0%, rgba(250, 247, 235, .93) 100%);
  -webkit-backdrop-filter: blur(16px) saturate(180%); backdrop-filter: blur(16px) saturate(180%);
  box-shadow: 0 8px 32px rgba(0, 0, 0, .06), inset 0 1px 0 rgba(255, 255, 255, .6);
}
.pear-help-widget__popover[hidden] { display: none; }

.pear-help-widget__header {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 12px;
  padding-bottom: 10px; margin-bottom: 8px; border-bottom: 1px solid var(--glass-line);
}
.pear-help-widget__title {
  margin: 0; font-family: var(--font-ui); font-weight: 800; font-size: 1rem; color: var(--help-accent);
}
.pear-help-widget__close {
  flex: none; width: 26px; height: 26px; border-radius: 50%; border: 0; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--muted); font-size: 1.2rem; line-height: 1; cursor: pointer;
  transition: background .2s var(--ease), color .2s var(--ease);
}
.pear-help-widget__close:hover { background: var(--pear-soft); color: var(--help-accent); }

.pear-help-widget__steps {
  margin: 0; padding-inline-start: 20px; display: flex; flex-direction: column; gap: 8px;
  font-family: var(--font-ui); font-size: .86rem; line-height: 1.45; color: var(--text);
}
.pear-help-widget__steps li { padding-inline-start: 2px; }
.pear-help-widget__steps li::marker { color: var(--help-accent); font-weight: 700; }

@media (max-width: 480px) {
  .pear-help-widget { bottom: calc(14px + var(--safe-b)); inset-inline-start: 14px; }
  .pear-help-widget__btn { width: 38px; height: 38px; font-size: 1.02rem; }
  .pear-help-widget__popover { width: min(300px, calc(100vw - 28px)); padding: 14px; }
}

/* STICKING/OVERLAP FIX: .profile-btn is position:fixed at a CONSTANT top offset,
   independent of .app-header's own scroll-triggered compaction (padding shrinks,
   see .app-header.is-scrolled) - so as the header shrank on scroll, the button
   stayed rigidly where it started, drifting out of sync with (and at some scroll
   positions, overlapping) header/focus-bar content that had since moved up under
   it. body.is-scrolled is set by the SAME scroll listener that drives
   .app-header.is-scrolled (see mobile-ux.js) - .profile-btn can't use a sibling
   selector off .app-header directly (it comes BEFORE it in the DOM; combinators
   only select forward), so this shared flag on <body> is what lets it react. */
@media (max-width: 768px) {
  body.is-scrolled .profile-btn {
    top: calc(8px + var(--safe-t)); width: 34px; height: 34px;
  }
  body.is-scrolled .profile-dropdown { top: calc(46px + var(--safe-t)); }
}

/* Vertically center the avatar on the focus-bar's own content row while
   they're both visible together (focus mode, unscrolled - once scrolled the
   avatar is faded out, see below, so no alignment to worry about there).
   .focus-bar is now 22px vertical padding (bumped from 15px - the bar sat too
   close to the viewport's top edge) + a ~24.6px content row (the badge is the
   tallest child: 5px+5px padding + a ~14.6px line) = ~68.6px tall, centered at
   ~34px from the top. The avatar's own base top:16px + half its 40px height =
   a 36px center, so this only needs a small nudge now (previously a bigger
   9px gap when the bar's padding was 15px). Only scoped >480px: below that
   the avatar is a smaller 32px circle (see the ≤480px "Profile icon" rule)
   with a different, already-close offset. */
@media (min-width: 481px) {
  body.focus-mode:not(.is-scrolled) .profile-btn { top: calc(var(--safe-t) + 14px); }
}

/* Focus mode + scrolled: the focus-bar's item-name/dot needs the WHOLE right
   edge (flush, zero gutter - see .focus-bar.is-scrolled below). Tried
   relocating the avatar to the opposite corner instead - it crowded the size
   badge sitting there and read as cramped. Simplest clean fix: fade the
   avatar out while scrolled; it's still one tap away (fades back in the
   instant the user scrolls back up, off the same class toggle). Applies at
   ANY width, not just mobile - desktop needs the same flush edge. Scale, not
   just opacity, so it reads as a deliberate "tuck away" rather than a flat
   dissolve - and its own transition (a quick, snappy --spring) so vanishing
   is brisk while the reappear (base rule above, --ease, no overshoot) is a
   calmer grow-in. */
body.focus-mode.is-scrolled .profile-btn {
  opacity: 0;
  transform: scale(.5);
  pointer-events: none;
  transition: transform .3s var(--spring), opacity .25s var(--ease);
}
.profile-btn__initial {
  font-family: var(--font-ui); font-weight: 800; font-size: .92rem; line-height: 1;
}

.profile-dropdown {
  /* Absolute for the same reason as .profile-btn above - the panel has to
     travel with the button it hangs from, so both must resolve against the
     same box. Leaving this `fixed` while the button scrolls away would tear
     the panel off its anchor and strand it mid-viewport. */
  position: absolute; top: 60px; right: 16px; z-index: 100;
  /* BUG FIX: was a hardcoded `direction: rtl` - the same bug just fixed on
     .cart-dropdown, which left this panel right-to-left even in English.
     `right:16px` above is deliberately NOT switched to a logical property:
     it pins to #profileBtn, which is itself `position:fixed; right:16px` in
     both directions, so the two must stay on the same physical edge. */
  direction: inherit;
  width: 220px; max-width: calc(100vw - 32px); padding: 16px;
  /* never run off a short viewport (landscape phone) - scroll inside instead */
  max-height: calc(100vh - 76px); overflow-y: auto;
  border: 1px solid var(--glass-border); border-radius: 12px;
  background: rgba(255, 255, 255, .97);
  -webkit-backdrop-filter: var(--blur-lg); backdrop-filter: var(--blur-lg);
  box-shadow: 0 4px 20px rgba(0, 0, 0, .15);
}
.profile-dropdown[hidden] { display: none; }

/* ── identity section - centred: big initials circle, full name, email ── */
.profile-dropdown__identity {
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  text-align: center; padding-bottom: 12px;
}
.profile-dropdown__avatar {
  width: 48px; height: 48px; margin-bottom: 6px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; background: var(--glass-dark); color: #fff;
  font-family: var(--font-ui); font-weight: 800; font-size: 1.05rem;
}
.profile-dropdown__name {
  font-family: var(--font-ui); font-weight: 800; font-size: .96rem; color: var(--ink);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.profile-dropdown__email {
  font-family: var(--font-ui); font-size: .78rem; color: var(--muted);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.profile-dropdown__divider { height: 1px; margin: 4px 0 8px; background: var(--glass-line); }

/* ── measurements section - right-aligned rows ── */
/* `start`, not `right` - follows the inherited direction now that the panel
   no longer forces RTL on itself (see .profile-dropdown above). */
.profile-dropdown__measurements { text-align: start; }
.profile-dropdown__row {
  display: flex; align-items: baseline; gap: 6px; padding: 4px 0;
  font-family: var(--font-ui); font-size: .82rem;
}
.profile-dropdown__label { color: var(--muted); flex: none; }
.profile-dropdown__value {
  color: var(--ink); font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.profile-dropdown__logout {
  width: 100%; margin-top: 10px; padding: 9px 12px; border: 0; border-radius: var(--pill);
  background: rgba(220, 60, 60, .12); color: #b23a3a;
  font-family: "Inter", sans-serif; font-size: .84rem; font-weight: 700; cursor: pointer;
  transition: background .25s var(--ease);
}
.profile-dropdown__logout:hover { background: rgba(220, 60, 60, .2); }

/* ── 1 · "Add to Cart" morphing button ─────────────────────────────────── */
.add-cart-row { max-width: 470px; margin: 14px auto 0; }
.btn-add-cart {
  position: relative; overflow: hidden;
  width: 100%; min-height: 56px; padding: 16px 24px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid rgba(141,182,0,.4); border-radius: var(--pill);
  background: linear-gradient(135deg, rgba(141,182,0,.22), rgba(141,182,0,.10));
  -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur);
  color: var(--pear-deep); cursor: pointer;
  font-family: "Urbanist", sans-serif; font-weight: 800; font-size: 1.04rem; letter-spacing: .01em;
  box-shadow: var(--shadow), inset 0 1px 0 rgba(255,255,255,.55);
  transition: transform .45s var(--power), background .4s var(--ease),
              border-color .4s var(--ease), box-shadow .4s var(--ease), color .3s var(--ease);
}
.btn-add-cart:hover  { transform: translateY(-2px); background: linear-gradient(135deg, rgba(141,182,0,.30), rgba(141,182,0,.14)); border-color: rgba(141,182,0,.6); box-shadow: 0 14px 34px rgba(141,182,0,.24), inset 0 1px 0 rgba(255,255,255,.6); }
.btn-add-cart:active { transform: scale(.96); }
.btn-add-cart:focus-visible { outline: none; box-shadow: 0 0 0 4px var(--pear-ring), var(--shadow); }
.bac__en   { font-weight: 600; font-size: .76rem; opacity: .68; letter-spacing: .04em; }
.bac__icon { font-weight: 700; margin-inline-end: 2px; }
.bac__label { display: inline-flex; align-items: center; gap: 7px; transition: opacity .3s var(--ease), transform .4s var(--power); }
/* spinner + check are centred overlays so the button width never jumps */
.bac__spinner, .bac__check {
  position: absolute; top: 50%; left: 50%;
  opacity: 0; pointer-events: none;
}
.bac__spinner {
  width: 22px; height: 22px; margin: -11px 0 0 -11px;
  border: 2.5px solid rgba(141,182,0,.28);
  border-top-color: var(--pear-deep); border-radius: 50%;
  transform: scale(.5);
}
.bac__check {
  margin: -0.62em 0 0 -0.42em;
  font-size: 1.3rem; font-weight: 900; color: var(--pear-deep);
  transform: scale(.4);
}
/* loading: label glides out, spinner fades in + spins, control "scales down" */
.btn-add-cart.is-loading { transform: scale(.965); pointer-events: none; }
.btn-add-cart.is-loading .bac__label { opacity: 0; transform: scale(.9); }
.btn-add-cart.is-loading .bac__spinner { opacity: 1; transform: scale(1); animation: bacSpin .8s linear infinite; }
@keyframes bacSpin { to { transform: scale(1) rotate(360deg); } }
/* done: checkmark pops with an elastic overshoot */
.btn-add-cart.is-done {
  background: linear-gradient(135deg, var(--pear), var(--pear-deep));
  border-color: transparent; color: #fff;
  box-shadow: var(--shadow-pear);
}
.btn-add-cart.is-done .bac__label   { opacity: 0; transform: scale(.9); }
.btn-add-cart.is-done .bac__spinner { opacity: 0; animation: none; }
.btn-add-cart.is-done .bac__check   { opacity: 1; color: #fff; animation: bacCheck .55s var(--spring) both; }
@keyframes bacCheck {
  0%   { opacity: 0; transform: scale(.2) rotate(-20deg); }
  60%  { opacity: 1; transform: scale(1.25) rotate(4deg); }
  100% { opacity: 1; transform: scale(1) rotate(0); }
}

/* the mini garment clone that flies into the cart */
.cart-fly {
  position: fixed; z-index: 9998; width: 64px; height: 80px;
  pointer-events: none; border-radius: 14px; overflow: hidden;
  border: 1px solid var(--glass-border);
  background: var(--glass-strong);
  box-shadow: var(--shadow-lift);
  transform: translate(-50%, -50%);
  will-change: transform, opacity;
}
.cart-fly img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cart-fly--dot {
  width: 26px; height: 26px; border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, #9cca00, var(--pear) 55%, var(--pear-deep));
  box-shadow: 0 6px 18px var(--pear-glow), inset 0 1px 2px rgba(255,255,255,.5);
}

/* ── 4 · Top-center spring "cart" toast (glassmorphism) ────────────────── */
.lux-cart-toast {
  position: fixed; top: calc(18px + var(--safe-t, 0px)); left: 50%; z-index: 300;
  display: inline-flex; align-items: center; gap: 10px;
  padding: 13px 22px; border-radius: var(--pill);
  background: linear-gradient(135deg, rgba(255,255,255,.55), rgba(255,255,255,.32));
  -webkit-backdrop-filter: blur(24px) saturate(200%); backdrop-filter: blur(24px) saturate(200%);
  border: 1px solid var(--glass-border);
  color: var(--ink); font-family: var(--font-ui); font-weight: 800; font-size: .92rem;
  box-shadow: var(--shadow-lift), inset 0 1px 0 rgba(255,255,255,.6);
  opacity: 0; pointer-events: none;
  transform: translate(-50%, -130%);
  transition: opacity .35s var(--ease), transform .55s var(--spring);
  max-width: 90vw;
}
.lux-cart-toast.show { opacity: 1; transform: translate(-50%, 0); }
.lux-cart-toast__icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%; flex-shrink: 0;
  background: linear-gradient(135deg, var(--pear), var(--pear-deep));
  color: #fff; font-size: .8rem; font-weight: 900;
  box-shadow: 0 3px 10px rgba(141,182,0,.4);
}

/* ── 2 · "LIQUID BRAND APERTURE" transition - GPU-ONLY (transform + opacity) ──
   Hardware-acceleration protocol: NOTHING that triggers layout/paint is animated
   (no width/height/clip-path/border-radius/SVG-r). Three composited layers:
     · __bg    - heavy glass backdrop (opacity only)
     · __shape - the brand logo        (transform scale + opacity)
     · __flood - pear-green aperture    (transform scale + opacity)
   The flood is ONE viewport-sized ellipse promoted to its own GPU layer; it
   scales up to mask the screen, then scales out + fades like a camera aperture.
   NO secondary "bite" shape exists any more - it was deleted entirely. */
.pear-transition {
  position: fixed; inset: 0; z-index: 9000;
  display: grid; place-items: center; pointer-events: none;
  transform: translate3d(0, 0, 0);          /* promote the overlay to a layer */
  transform-style: preserve-3d;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
  will-change: transform, opacity;
}
.pear-transition[hidden] { display: none; }
.pear-transition__bg {
  position: absolute; inset: 0; opacity: 0;
  background:
    radial-gradient(60% 50% at 50% 42%, rgba(120,104,72,.14), transparent 70%),
    linear-gradient(180deg, rgba(244,238,225,.98), rgba(236,228,210,.98));
  -webkit-backdrop-filter: blur(22px) saturate(180%); backdrop-filter: blur(22px) saturate(180%);
  will-change: opacity;
}
.pear-transition__shape {
  position: relative;
  width: clamp(190px, 30vmin, 320px); height: auto;
  transform-origin: 50% 52%;
  transform: translate3d(0, 0, 0) scale(.15); opacity: 0;
  filter: drop-shadow(0 24px 60px rgba(141,182,0,.4));
  will-change: transform, opacity;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
}
/* The pear-green flood/aperture: a single viewport-sized ellipse (cheap layer)
   scaled from the centre. transform + opacity only. */
.pear-transition__flood {
  position: fixed; inset: 0;
  border-radius: 50%;                        /* static - never animated */
  background:
    radial-gradient(circle at 50% 42%, #a4d000 0%, var(--pear) 52%, var(--pear-deep) 100%);
  transform-origin: 50% 50%;
  transform: translate3d(0, 0, 0) scale(0);
  opacity: 1;
  will-change: transform, opacity;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
}
/* ── Cinematic 1200ms sequence - Intro → Flood → (swap) → Aperture reveal ────
   Phase 1  INTRO    (0–400ms):   logo scales up + fades in over the glass bg.
   Phase 2  FLOOD    (400–700ms): the pear-green ellipse scales up to fully mask
            the viewport; the logo + bg fade out underneath, hidden by the flood.
   Phase 3  SWAP     (≈700ms):    app.js swaps screens while 100% covered (rAF).
   Phase 4  APERTURE (700–1200ms): the flood scales to ×3.5 and fades out on the
            Apple curve - a luxury aperture opening onto the try-on room. */
.pear-transition.is-playing .pear-transition__bg {
  animation: apBg 1.2s linear both;
}
.pear-transition.is-playing .pear-transition__shape {
  animation: apLogoIn .4s var(--power) both,
             apLogoOut .2s linear .42s forwards;   /* fades just before the flood seals */
}
.pear-transition.is-playing .pear-transition__flood {
  animation: apFloodIn .3s var(--power) .4s both,
             apFloodOut .5s var(--ease) .7s forwards;   /* --ease = cubic-bezier(.16,1,.3,1) */
}
@keyframes apBg {
  0%   { opacity: 0; }
  12%  { opacity: 1; }   /* glass backdrop in by ~145ms */
  56%  { opacity: 1; }   /* hold until the flood has sealed the viewport (~675ms) */
  58%  { opacity: 0; }   /* then drop instantly, hidden behind the opaque flood */
  100% { opacity: 0; }
}
@keyframes apLogoIn {
  0%   { transform: translate3d(0,0,0) scale(.4)  rotate(-4deg); opacity: 0; }
  60%  { transform: translate3d(0,0,0) scale(1.04) rotate(1deg); opacity: 1; }
  100% { transform: translate3d(0,0,0) scale(1)   rotate(0deg);  opacity: 1; }
}
@keyframes apLogoOut { to { opacity: 0; } }   /* covered by the flood as it fades */
@keyframes apFloodIn {        /* liquid flood - seals the viewport from the centre */
  0%   { transform: translate3d(0,0,0) scale(0);   opacity: 1; }
  100% { transform: translate3d(0,0,0) scale(1.7); opacity: 1; }   /* >diagonal → full cover */
}
@keyframes apFloodOut {       /* cinematic aperture - scale out + fade, Apple curve */
  0%   { transform: translate3d(0,0,0) scale(1.7); opacity: 1; }
  100% { transform: translate3d(0,0,0) scale(3.5); opacity: 0; }
}

/* ── 4 · Floating-label fields ─── base only; the magnetic micro-inertia
   behaviour (label glides toward the cursor) is defined in the palette section
   below so it can use the --lblx/--lbly custom properties set by JS. ───────── */
.lux-field { position: relative; }

/* ── 4 · Metallic skeleton shimmer ─────────────────────────────────────── */
.lux-skeleton { position: relative; overflow: hidden; }
.lux-skeleton::after {
  content: ""; position: absolute; inset: 0; z-index: 2;
  background: linear-gradient(110deg,
    rgba(230,230,232,.55) 0%, rgba(248,248,250,.9) 18%,
    rgba(255,255,255,.95) 30%, rgba(248,248,250,.9) 42%, rgba(230,230,232,.55) 60%);
  background-size: 220% 100%;
  animation: luxShimmer 1.5s var(--ease) infinite;
}
@keyframes luxShimmer { from { background-position: 180% 0; } to { background-position: -60% 0; } }

/* ── 4 · Magnetic buttons - animate the inner content drift smoothly ───── */
.lux-magnetic > * { transition: transform .45s var(--power); }
.lux-magnetic:active > * { transition-duration: .12s; }

/* ── accessibility: collapse the heavier theatre under reduced-motion ──── */
@media (prefers-reduced-motion: reduce) {
  .pear-transition { display: none !important; }
  .cart-btn.is-bounce, .btn-add-cart.is-done .bac__check, .lux-skeleton::after { animation: none; }
}

/* ════════════════════════════════════════════════════════════════════════
   ✦ BRAND PALETTE - logo-matched ivory + crisp black + PEAR-GREEN accent
   ─────────────────────────────────────────────────────────────────────────
   Strictly three colours, derived from the brand assets:
     • Canvas      - warm luxury IVORY (the logo's cream ground).
     • Type / lines- crisp luxury BLACK (the logo's line-art ink).
     • Accent      - vibrant PEAR GREEN (#8DB600), reserved for interactive,
                     active and highlight moments only (buttons, focus, live,
                     active tiles, results, progress). NO other accent colours.
   This block lives LAST so it is the single source of truth for the palette.
   ════════════════════════════════════════════════════════════════════════ */
:root {
  /* warm ivory canvas */
  --bg:        #efe8d8;
  --bg-soft:   rgba(255, 252, 244, .5);

  /* crisp luxury black ink scale (slightly warm, from the logo) */
  --ink:    #0a0a0b;
  --text:   #161512;
  --muted:  #6c665b;
  --muted-2:#a89f8d;

  /* PEAR-GREEN accent family - the one sanctioned accent */
  --pear:      #8DB600;
  --pear-deep: #5f7d00;
  --pear-soft: rgba(141, 182, 0, .10);
  --pear-tint: rgba(141, 182, 0, .14);
  --pear-ring: rgba(141, 182, 0, .22);
  --pear-glow: rgba(141, 182, 0, .50);
  --shadow-pear: 0 12px 36px rgba(141, 182, 0, .26);

  /* warm frosted ivory "Liquid Glass" (blur + saturation already in --blur) */
  --glass:        rgba(255, 252, 244, .55);
  --glass-strong: rgba(255, 252, 244, .72);
  --glass-soft:   rgba(255, 252, 244, .40);
  --glass-border: rgba(255, 255, 255, .60);
  --glass-line:   rgba(22, 19, 12, .13);
}

/* ivory canvas + soft warm light with a faint pear-green brand wash */
body {
  background:
    radial-gradient(1100px 760px at 84% -10%, rgba(141, 182, 0, .10), transparent 60%),
    radial-gradient(900px 700px  at -8% 14%,  rgba(22, 19, 12, .05),  transparent 58%),
    radial-gradient(900px 900px  at 50% 118%, rgba(141, 182, 0, .06), transparent 60%),
    linear-gradient(180deg, #f4eee1 0%, #ece4d2 52%, #f1ebdd 100%);
  background-attachment: fixed;
}
/* ambient orbs: one warm sand, one faint pear-green - subtle, never an accent */
body::before { background: radial-gradient(circle at 30% 30%, rgba(141, 182, 0, .26), transparent 65%); }
body::after  { background: radial-gradient(circle at 60% 40%, rgba(150, 132, 92, .22), transparent 65%); }

/* the brand mark stays crisp black line-art, exactly like the logo */
.brand-mark {
  width: 30px; height: auto; flex-shrink: 0; align-self: center;
  color: var(--ink); margin-inline-end: 4px;
}

/* ── ADD-TO-CART CTA - pear-green physical-glass feedback ────────────────── */
.btn-add-cart {
  border-color: rgba(141, 182, 0, .40);
  background: linear-gradient(135deg, rgba(141, 182, 0, .20), rgba(141, 182, 0, .08));
  color: var(--pear-deep);
}
.btn-add-cart:hover {
  background: linear-gradient(135deg, rgba(141, 182, 0, .30), rgba(141, 182, 0, .13));
  border-color: rgba(141, 182, 0, .60);
  box-shadow: 0 14px 34px rgba(141, 182, 0, .26), inset 0 1px 0 rgba(255, 255, 255, .6);
}
.btn-add-cart:focus-visible { box-shadow: 0 0 0 4px var(--pear-ring), var(--shadow); }
.bac__spinner { border-color: rgba(141, 182, 0, .28); border-top-color: var(--pear-deep); }
.bac__check   { color: var(--pear-deep); }
.btn-add-cart.is-done {                      /* satisfying success fill */
  background: linear-gradient(135deg, var(--pear), var(--pear-deep));
  border-color: transparent; color: #fff;
  box-shadow: var(--shadow-pear);
}
.btn-add-cart.is-done .bac__check { color: #fff; }

.cart-btn__count {
  /* LEGIBILITY FIX: the lighter end of the old pear→pear-deep gradient put white
     text at ~2.4:1 contrast - well under the 4.5:1 AA minimum. A flat
     --pear-deep keeps the same brand color family at a safe ~4.75:1. */
  background: var(--pear-deep);
  box-shadow: 0 3px 10px rgba(141, 182, 0, .45);
}
.cart-fly--dot {
  background: radial-gradient(circle at 34% 30%, #b6e000, var(--pear) 55%, var(--pear-deep));
  box-shadow: 0 6px 18px var(--pear-glow), inset 0 1px 2px rgba(255, 255, 255, .5);
}
.lux-cart-toast__icon {
  background: linear-gradient(135deg, var(--pear), var(--pear-deep));
  box-shadow: 0 3px 10px rgba(141, 182, 0, .40);
}

/* ════════════════════════════════════════════════════════════════════════
   ✦ OVERFLOW ISOLATION - content can never collide with the ambient rails
   ─────────────────────────────────────────────────────────────────────────
   The rails are fixed at the viewport edges (z-index 0, strictly behind the
   content). The only at-risk element is the full-bleed sticky focus bar, whose
   long RTL string used to run under the side tracks (see edited-image.png).
   We (a) reserve a safe gutter wider than the rails on large screens and
   (b) let the focused-item line truncate cleanly instead of crossing them.
   ════════════════════════════════════════════════════════════════════════ */
.focus-bar { gap: 12px; }
.focus-bar__text {
  min-width: 0; flex: 1 1 auto;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.focus-bar__badge { flex-shrink: 0; }
/* once the rails appear (≥1340px) keep every full-width bar inside the gutter.
   Split left/right instead of the symmetric padding-inline shorthand: the
   right side only ever needs to clear the fixed avatar (40px + 16px inset =
   56px), which the wide rail clamp badly over-reserved - dot+text ended up
   with a big dead gap before the avatar instead of sitting close to it. Left
   keeps the full rail clamp (still guards the badge side). */
@media (min-width: 1340px) {
  .focus-bar {
    /* BUG FIX: floor raised 84px -> 136px - the old floor was sized only for
       the ambient rail, not for .lang-switch (fixed top:16px;left:16px,
       ~136px clearance needed - see the ≤1339px rule above for the exact
       arithmetic), so .focus-bar__badge still rendered under the language
       toggle for 1340-~1885px wide viewports (where 7vw < 136px). The cap
       (150px) and the growth curve above the new floor are unchanged. */
    padding-left: clamp(136px, 7vw, 150px);
    padding-right: 72px;   /* 56px avatar clearance + a deliberate ~16px breathing gap */
  }
}

/* ── Real-logo loaders (/pear-logo.png) - replace the emoji pear ──────────── */
img.pear-loader__fruit { width: 46px; height: auto; object-fit: contain; }
/* the loader sits on the DARK scan overlay → render the black logo as white */
.scan-overlay img.pear-loader__fruit {
  filter: brightness(0) invert(1) drop-shadow(0 8px 12px rgba(0, 0, 0, .30));
}

/* ════════════════════════════════════════════════════════════════════════
   ✦ LIQUID-GLASS DYNAMIC BACKGROUND - mouse/tilt-reactive ambient mesh
   A single GPU layer (transform/opacity only) of blurred light blobs. JS gives
   it an inertial translate3d so it reads like light moving under liquid glass.
   ════════════════════════════════════════════════════════════════════════ */
.liquid-mesh {
  position: fixed; inset: -14%; z-index: 0; pointer-events: none;
  background:
    radial-gradient(38% 38% at 22% 26%, rgba(141, 182, 0, .16), transparent 70%),
    radial-gradient(42% 42% at 82% 20%, rgba(150, 132, 92, .18), transparent 72%),
    radial-gradient(48% 48% at 70% 84%, rgba(141, 182, 0, .10), transparent 72%),
    radial-gradient(40% 40% at 14% 80%, rgba(22, 19, 12, .06),  transparent 72%);
  /* Was blur(64px) with `will-change: transform`. This element is fixed at
     inset:-14% (≈128% of the viewport on both axes, ~3.4Mpx at 1080p), so a
     large-radius blur here is one of the most expensive things on the page -
     and `will-change` kept it permanently promoted to its own GPU layer for
     the whole session, on every screen, blur buffers included.

     translate3d(0,0,0) below already promotes the layer for the duration of
     the parallax drift (lux-interactions.js writes translate3d on this
     element), which is what the animation actually needs; `will-change` on
     top of that only pinned the memory earlier and for longer. Dropping the
     blur to 48px cuts the intermediate buffer work noticeably - the mesh is a
     diffuse 4-gradient wash, so the visual difference is imperceptible while
     saturate(160%) keeps the colour identical. */
  filter: blur(48px) saturate(160%);
  transform: translate3d(0, 0, 0);
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
}
@media (prefers-reduced-motion: reduce) { .liquid-mesh { transform: none !important; } }

/* ── Tactile Add-to-Cart: soft pear-green "fluid glow" pulse while working ── */
.btn-add-cart.is-loading { animation: bacGlow 1.5s var(--ease) infinite; }
@keyframes bacGlow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(141, 182, 0, 0), var(--shadow); }
  50%      { box-shadow: 0 0 0 6px rgba(141, 182, 0, .12), 0 10px 30px rgba(141, 182, 0, .30); }
}

/* ── Micro-inertia floating label - glides magnetically toward the cursor ── */
.lux-field > label {
  transform-origin: center;
  transform: translate3d(var(--lblx, 0px), var(--lbly, 0px), 0);
  transition: transform .2s var(--ease), color .3s var(--ease), letter-spacing .3s var(--ease);
  will-change: transform;
}
.lux-field.is-focus > label,
.lux-field.is-filled > label {
  transform: translate3d(var(--lblx, 0px), calc(var(--lbly, 0px) - 3px), 0) scale(.92);
  color: var(--pear-deep); letter-spacing: .01em;
}
.lux-field.is-focus > label { color: var(--pear); }

/* ════════════════════════════════════════════════════════════════════════
   "UPLOAD YOUR OWN GARMENT" - upload card · detection overlay · boxes
   ────────────────────────────────────────────────────────────────────────
   Its own Luxury white / royal-blue accent (scoped --royal* tokens), kept
   distinct from the app's pear-green so the upload flow reads as a special,
   premium action while the rest of the room is untouched.
   ════════════════════════════════════════════════════════════════════════ */
:root {
  --royal:      #0b3c95;                 /* MERIDIAN royal blue - borders / text */
  --royal-lift: #2b6bff;                 /* brighter royal - glow / active state */
  --royal-soft: rgba(43, 107, 255, .14);
  --royal-ring: rgba(43, 107, 255, .30);
  --royal-glow: rgba(43, 107, 255, .55);
}

/* ── Upload card (first tile in the catalog grid) ─────────────────────────── */
.cat-item--upload {
  border: 1.5px dashed var(--royal);
  background: linear-gradient(155deg, rgba(43,107,255,.10), rgba(255,255,255,.5));
}
.cat-item--upload:hover {
  border-color: var(--royal-lift);
  box-shadow: 0 0 0 2px var(--royal-ring), 0 18px 44px rgba(11,60,149,.22);
}
.cat-item--upload.is-active { animation: none; }
.cat-upload__media {
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(120% 120% at 50% 15%, rgba(43,107,255,.16), transparent 62%);
}
.cat-upload__icon {
  display: grid; place-items: center; width: 54px; height: 54px; border-radius: 50%;
  color: var(--royal); background: rgba(255,255,255,.7);
  box-shadow: 0 6px 18px rgba(11,60,149,.18), inset 0 0 0 1px rgba(43,107,255,.25);
  transition: transform .4s var(--ease), box-shadow .4s var(--ease), color .3s var(--ease);
}
.cat-item--upload:hover .cat-upload__icon {
  transform: translateY(-3px) scale(1.06); color: var(--royal-lift);
  box-shadow: 0 10px 26px rgba(11,60,149,.28), inset 0 0 0 1px rgba(43,107,255,.4);
}
.cat-item--upload .cat-item__name { color: var(--royal); }
.cat-upload__en { color: var(--royal-lift) !important; font-weight: 600; }

/* ── Detection overlay ─────────────────────────────────────────────────────
   Fade in/out is driven entirely by the .show class + CSS transitions (no JS
   timers). The initial `hidden` attribute is removed on first open; thereafter
   .show alone governs visibility, so the close animation plays with zero
   hardcoded teardown timing. */
.gd-overlay {
  position: fixed; inset: 0; z-index: 400;
  display: flex; align-items: center; justify-content: center;
  padding: clamp(14px, 4vw, 40px);
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .28s var(--ease), visibility .28s var(--ease);
}
.gd-overlay[hidden] { display: none; }
.gd-overlay.show { opacity: 1; visibility: visible; pointer-events: auto; }
.gd-overlay__backdrop {
  position: absolute; inset: 0; background: rgba(6,14,32,.58);
  -webkit-backdrop-filter: blur(22px) saturate(180%); backdrop-filter: blur(22px) saturate(180%);
}
.gd-overlay__panel {
  position: relative; z-index: 1;
  width: min(720px, 96vw); max-height: 92vh; overflow-y: auto;
  display: flex; flex-direction: column; gap: 16px;
  padding: clamp(16px, 3vw, 24px);
  background: linear-gradient(150deg, rgba(255,255,255,.92), rgba(244,248,255,.86));
  border: 1px solid rgba(43,107,255,.22); border-radius: 26px;
  box-shadow: 0 30px 80px rgba(6,14,32,.42), inset 0 1px 0 rgba(255,255,255,.7);
}
.gd-overlay.show .gd-overlay__panel { animation: gdPanelIn .5s cubic-bezier(.22,1,.3,1) both; }
@keyframes gdPanelIn { from { opacity: 0; transform: translateY(18px) scale(.97); } to { opacity: 1; transform: none; } }

/* heading settles a beat after .gd-overlay__panel itself, echoing the same
   card-then-heading ordering used on screen swap above */
.gd-head { display: flex; align-items: flex-start; gap: 12px; animation: fade .4s var(--ease) both .06s; }
.gd-eyebrow {
  display: inline-block; font-size: .64rem; font-weight: 800; letter-spacing: .18em;
  text-transform: uppercase; color: var(--royal-lift);
}
.gd-title { margin: 4px 0 2px; font-size: 1.16rem; font-weight: 800; color: var(--royal); }
.gd-title span:not([data-i18n]) { color: var(--muted); font-weight: 700; font-size: .82rem; }
.gd-sub { margin: 0; font-size: .82rem; color: var(--muted); }
.gd-close {
  margin-inline-start: auto; flex: none; width: 38px; height: 38px; border-radius: 50%;
  border: 1px solid rgba(43,107,255,.28); background: #fff; color: var(--royal);
  font-size: 1.05rem; line-height: 1; cursor: pointer;
  transition: transform .5s var(--ease), background .3s var(--ease);
}
.gd-close:hover { transform: scale(1.08) rotate(90deg); background: var(--royal-soft); }
.gd-close:active { transform: scale(.94); }

/* ── TOP / BOTTOM segmented toggle (outfit mode) - luxury pill control ──────── */
.gd-tabs {
  position: relative; align-self: center; display: grid; grid-template-columns: 1fr 1fr;
  width: min(300px, 100%); padding: 4px; border-radius: var(--pill);
  background: var(--royal-soft); border: 1px solid rgba(43,107,255,.2);
  box-shadow: inset 0 1px 3px rgba(6,14,32,.08);
}
.gd-tabs[hidden] { display: none; }
.gd-tab {
  position: relative; z-index: 1; padding: 6px 10px; border-radius: var(--pill);
  display: flex; flex-direction: column; align-items: center; line-height: 1.05;
  font-size: .82rem; font-weight: 800; color: var(--royal);
  background: transparent; border: none; cursor: pointer;
  transition: color .25s var(--ease);
}
.gd-tab small { font-size: .56rem; font-weight: 700; opacity: .6; letter-spacing: .1em; text-transform: uppercase; }
.gd-tab.is-active { color: #fff; }
.gd-tab.is-active small { opacity: .85; }
.gd-tabs__ind {
  position: absolute; z-index: 0; top: 4px; bottom: 4px; inset-inline-start: 4px; width: calc(50% - 4px);
  border-radius: var(--pill); background: var(--royal-lift);
  box-shadow: 0 6px 16px rgba(11,60,149,.35);
  transition: transform .32s var(--ease);
}
/* RTL-aware: the pill slides toward the "Bottom" tab (second column). */
.gd-tabs[data-active="bottom"] .gd-tabs__ind { transform: translateX(100%); }
[dir="rtl"] .gd-tabs[data-active="bottom"] .gd-tabs__ind { transform: translateX(-100%); }

/* stage + tightly-wrapped image frame (boxes overlay the image exactly) */
.gd-stage { position: relative; display: flex; align-items: center; justify-content: center; min-height: 160px; }
.gd-frame { position: relative; display: inline-block; line-height: 0; border-radius: 16px; overflow: hidden; box-shadow: var(--shadow-soft); }
.gd-frame__img { display: block; max-width: 100%; max-height: min(64vh, 560px); width: auto; height: auto; }

/* ── focus scrim - subtle dark tint over the whole image (viewfinder mode) ──── */
.gd-scrim {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: rgba(6,14,32,.44);
  transition: background .3s var(--ease);
}
/* Deepen the tint on the rest of the image while any bracket is hovered. */
.gd-frame:has(.gd-box:hover) .gd-scrim,
.gd-frame:has(.gd-box:focus-visible) .gd-scrim { background: rgba(6,14,32,.6); }

.gd-boxes { position: absolute; inset: 0; z-index: 2; }

/* ── viewfinder bracket - no harsh border; rounded corner crosshairs + glow ── */
.gd-box {
  position: absolute; margin: 0; padding: 0; cursor: pointer;
  background: transparent; border: none; border-radius: 16px;
  transition: transform .25s var(--ease), -webkit-backdrop-filter .3s var(--ease), backdrop-filter .3s var(--ease),
    left .38s var(--ease), top .38s var(--ease), width .38s var(--ease), height .38s var(--ease);
  animation: gdBoxIn .4s var(--ease) both; animation-delay: calc(var(--i, 0) * .06s);
}
@keyframes gdBoxIn { from { opacity: 0; transform: scale(.92); } to { opacity: 1; transform: scale(1); } }
/* soft pulsating glow ring behind the corners */
.gd-box::before {
  content: ""; position: absolute; inset: 0; border-radius: 16px; pointer-events: none;
  box-shadow: 0 0 0 1px rgba(255,255,255,.16), 0 0 14px rgba(43,107,255,.22);
  animation: gdGlow 2.6s ease-in-out infinite;
}
@keyframes gdGlow {
  0%, 100% { box-shadow: 0 0 0 1px rgba(255,255,255,.16), 0 0 12px rgba(43,107,255,.22); }
  50%      { box-shadow: 0 0 0 1px rgba(255,255,255,.28), 0 0 26px rgba(43,107,255,.5); }
}
/* Hover / focus: clear this zone's dark tint (spotlight) + intensify the brackets. */
.gd-box:hover, .gd-box:focus-visible {
  outline: none; z-index: 3; transform: scale(1.012);
  -webkit-backdrop-filter: brightness(1.5) saturate(1.12); backdrop-filter: brightness(1.5) saturate(1.12);
}
.gd-box:active { transform: scale(.99); }

/* rounded corner crosshairs (white brackets w/ royal glow) */
.gd-corner {
  position: absolute; width: 26px; height: 26px; pointer-events: none;
  border: 3px solid #fff; opacity: .88;
  filter: drop-shadow(0 0 6px rgba(43,107,255,.65));
  transition: width .25s var(--ease), height .25s var(--ease), opacity .25s var(--ease), border-color .25s var(--ease);
}
.gd-corner--tl { top: -2px; left: -2px;  border-right: none;  border-bottom: none; border-top-left-radius: 13px; }
.gd-corner--tr { top: -2px; right: -2px; border-left: none;   border-bottom: none; border-top-right-radius: 13px; }
.gd-corner--bl { bottom: -2px; left: -2px;  border-right: none; border-top: none;  border-bottom-left-radius: 13px; }
.gd-corner--br { bottom: -2px; right: -2px; border-left: none;  border-top: none;  border-bottom-right-radius: 13px; }
.gd-box:hover .gd-corner, .gd-box:focus-visible .gd-corner {
  width: 34px; height: 34px; opacity: 1;
  filter: drop-shadow(0 0 10px rgba(43,107,255,.9));
}

/* label badge ("עליון · Top" / "תחתון · Bottom" / "מלא · Full-body") - always shown
   so overlapping garments are unambiguous; lights up on hover. */
.gd-box__label {
  position: absolute; top: 8px; inset-inline-start: 8px;
  display: inline-flex; align-items: baseline; gap: 5px;
  padding: 4px 11px; border-radius: var(--pill);
  font-size: .7rem; color: #0b1b3a; background: rgba(255,255,255,.94);
  box-shadow: 0 4px 14px rgba(6,14,32,.4); backdrop-filter: blur(4px);
  transition: background .25s var(--ease), color .25s var(--ease), transform .25s var(--ease);
}
.gd-box__label b { font-weight: 800; letter-spacing: .01em; }
.gd-box__label span { font-size: .6rem; font-weight: 700; opacity: .6; text-transform: uppercase; letter-spacing: .06em; }
.gd-box:hover .gd-box__label, .gd-box:focus-visible .gd-box__label {
  background: var(--royal-lift); color: #fff; transform: scale(1.05);
}
.gd-box:hover .gd-box__label span, .gd-box:focus-visible .gd-box__label span { opacity: .85; }

/* crisp click-confirmation flash before the modal closes */
.gd-box.is-picked { animation: gdPick .28s ease both; }
.gd-box.is-picked .gd-corner { border-color: var(--royal-lift); filter: drop-shadow(0 0 14px rgba(43,107,255,1)); }
@keyframes gdPick {
  0%   { transform: scale(1.012); }
  45%  { transform: scale(.955); -webkit-backdrop-filter: brightness(1.85); backdrop-filter: brightness(1.85); }
  100% { transform: scale(1.02);  -webkit-backdrop-filter: brightness(1.5);  backdrop-filter: brightness(1.5); }
}
@media (prefers-reduced-motion: reduce) {
  .gd-box::before, .gd-box.is-picked { animation: none; }
}

/* detecting spinner */
.gd-loading {
  position: absolute; inset: 0; z-index: 4; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
  background: rgba(255,255,255,.3); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
  color: var(--royal); font-weight: 700; font-size: .86rem;
}
.gd-loading[hidden] { display: none; }
.gd-spinner {
  width: 38px; height: 38px; border-radius: 50%;
  border: 3px solid rgba(43,107,255,.22); border-top-color: var(--royal-lift);
  animation: gdSpin .8s linear infinite;
}
@keyframes gdSpin { to { transform: rotate(360deg); } }

/* empty state */
.gd-empty {
  position: absolute; inset: 0; z-index: 5; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
  text-align: center; padding: 24px;
  background: rgba(248,250,255,.82); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  border-radius: 16px;
}
.gd-empty[hidden] { display: none; }
.gd-empty__icon { font-size: 2rem; }
.gd-empty__text { margin: 0; font-size: .92rem; font-weight: 700; color: var(--ink); }
.gd-empty__text span { color: var(--muted); font-weight: 600; font-size: .82rem; }
.gd-empty__retry { margin-top: 6px; }

.gd-hint { margin: 0; text-align: center; font-size: .8rem; color: var(--muted); }

body.gd-open { overflow: hidden; }

@media (max-width: 640px) {
  .gd-overlay__panel { width: 100%; max-height: 94vh; }
  .gd-frame__img { max-height: 56vh; }
  /* touch: no hover → keep the brackets crisp and lift the tint a touch so items read */
  .gd-corner { opacity: 1; }
  .gd-scrim { background: rgba(6,14,32,.32); }
}

/* ════════════════════════════════════════════════════════════════════════
   ✦ PEAR BRAND PASS - Didot wordmark · raster logo · camera stage · guide
   ════════════════════════════════════════════════════════════════════════ */

/* Didot serif on literal PEAR/pear brand text ONLY (never MERIDIAN). */
.pear-wordmark,
.app-header__brand,
.kicker,
.calc-editorial__kicker,
.gd-eyebrow {
  font-family: var(--pear-wordmark);
}

/* Header layout - three GENUINE flex siblings on one row: nav (right in RTL) ·
   brand-lockup (centre) · actions (left in RTL). This replaces an earlier
   design where the lockup was pulled out of flex flow via position:absolute
   and centred across the full header width - that made overlap with nav/
   actions a geometry problem (nothing pushed it aside when they grew), which
   is exactly what caused the edit-measurements button to render on top of the
   subtitle text. With all three as real flex items again, ordinary
   justify-content:space-between + min-width:0 makes overlap structurally
   impossible: the browser lays them out side by side and shrinks them (via
   the existing ellipsis truncation below) instead of ever letting one grow
   into another's space. flex:1 1 0 on the two side zones keeps them equal, so
   the lockup is genuinely centred whenever nav/actions are reasonably
   balanced, and gracefully shrinks itself (min-width:0) if space runs out. */
.app-header__nav,
.app-header__actions {
  display: flex; align-items: center; gap: 8px; min-width: 0;
  flex: 1 1 0;
  min-height: var(--hdr-row-h);   /* all three zones agree on the row - see --hdr-row-h */
}
/* Every direct child of the header, whatever it is, on the same centreline and
   with no margin of its own to shift it off. Stated once here rather than per
   control, so a control added later inherits the rhythm instead of having to
   remember it.
   NOTE margin, not padding: a blanket `padding: 0` would collapse the pill
   shapes these controls are built from (.btn-edit-measurements' 0 13px, the
   switches' --switch-pad track inset), so horizontal padding is deliberately
   left to each control and only stray MARGIN is normalised away. */
/* ── THE BLACK DOT, and why a blanket child rule caused it ─────────────────
   REPORTED: a black bullet between the language switch and the cart.

   It is not a list bullet and not a pseudo-element - it is #engineBadge, a real
   <span class="app-header__engine">, whose text content is a literal U+25CF
   BLACK CIRCLE glyph. It has been hidden by `.app-header__engine{display:none}`
   since it was retired, and it stayed hidden until the blanket normalisation
   rule below was added: `.app-header__actions > *` has the SAME specificity
   (0,1,0) as `.app-header__engine` and is declared ~2700 lines later, so it won
   on source order and set the badge back to inline-flex. A rule written to make
   the row tidy un-hid a retired element.

   Two guards, because either alone would let it back:
     · the selector excludes anything that is deliberately not a row item, so
       the rule cannot change the display of something meant to be hidden;
     · the display:none is re-asserted immediately AFTER it, so source order is
       working for us rather than against us the next time something sweeps
       these children.
   The real lesson is narrower than the fix: a blanket `> *` rule that sets
   `display` will resurrect anything hidden that way. Prefer excluding, and
   re-assert. */
.app-header__nav > *:not(.app-header__engine),
.app-header__actions > *:not(.cart-dropdown):not(.app-header__engine) {
  display: inline-flex; align-items: center; justify-content: center;
  margin: 0;
}
/* Retired status dot - re-asserted here, after the sweep above, deliberately. */
.app-header__engine { display: none !important; }
/* The cart dropdown is an absolutely-anchored popover, not a row item - it must
   not be forced inline-flex by the rule above or it lays out as a flex row. */
.app-header__actions > .cart-dropdown { display: block; }
.app-header__actions > .cart-dropdown[hidden] { display: none; }

/* List hygiene for the header. Nothing in it is a <ul>/<ol> today - the dot
   above was a glyph, not a marker - so this changes nothing now. It is here
   because a marker is the OTHER way a stray dot appears in a flex row, and a
   future control added as a list would inherit the browser default silently. */
.app-header ul, .app-header ol, .app-header li {
  list-style: none; list-style-type: none; margin: 0; padding: 0;
}
.app-header li::marker { content: none; }
.app-header__actions { justify-content: flex-end; position: relative; }   /* anchors .cart-dropdown */
.app-header__actions .cart-btn { margin: 0; }   /* zone handles placement now */
/* BUG FIX: the nav pills ("עריכת מידות", the fullscreen switch) used to be the
   shrink-and-ellipsize safety valve (see .btn-edit-measurements span below) -
   at ordinary desktop/laptop widths that meant their OWN label text quietly
   truncated to "עריכת..." well before the row was actually out of room. The
   brand lockup already has its own graceful shrink-and-ellipsize fallback
   (.app-header__brand/__sub, see the LAYOUT FIX comment below) and is purely
   decorative, so it - not an actionable button's label - should be the thing
   that gives way first. flex-shrink:0 here means these pills always render
   at their natural content width; only the lockup compresses under pressure. */
.app-header__nav > * { flex-shrink: 0; }

/* Brand lockup: logo + title/subtitle as one row. Logo is FIRST in DOM order,
   so in this RTL document it renders at the lockup's visual RIGHT edge - i.e.
   to the right of "PEAR" / the subtitle, vertically centred via
   align-items:center. flex:0 1 auto + min-width:0 lets the whole lockup
   shrink (and its title text ellipsize, see .app-header__brand/__sub) rather
   than force nav/actions to make room for it. */
.app-header__brand-lockup {
  display: flex; align-items: center; gap: 10px; flex: 0 1 auto; min-width: 0;
  min-height: var(--hdr-row-h);   /* the zone that used to SET the row now conforms to it */
}
.app-header__titles {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 3px; min-width: 0;
}
/* Header mark is a raster <img>; height-driven so its portrait width stays
   proportional. flex:none - the logo never shrinks/distorts; only the text
   beside it truncates under pressure. */
.brand-mark {
  width: auto; height: 38px; object-fit: contain; flex: none;
  /* align-self:center is what actually centres it - this is a flex item, so its
     baseline plays no part. vertical-align/auto margins are kept alongside for
     the same reason the rest of this block is explicit: if the lockup is ever
     un-flexed (an inline fallback, a print sheet), the image still sits on the
     centreline instead of dropping to the text baseline. */
  align-self: center; margin: auto 0; vertical-align: middle; display: block;
}

/* Desktop: widen the camera stage so landscape webcam + AI output show with far
   less crop/zoom than the mobile portrait box. */
@media (min-width: 768px) {
  .camera-card { max-width: 600px; aspect-ratio: 4 / 3; }
}

/* Phone: keep the logo proportional to the compacting sticky header.
   PROMINENCE FIX: was 32px/28px (scrolled) - scaled up to match the larger wordmark above. */
@media (max-width: 768px) {
  .brand-mark { height: 36px; }
  .app-header.is-scrolled .brand-mark { height: 30px; }
}

/* ════════════════════════════════════════════════════════════════════════
   ✦ MOBILE HEADER (≤768px, tightened further ≤480px) - ONE compact flex row
   ─────────────────────────────────────────────────────────────────────────
   SUPERSEDES a previous ≤480px design that deliberately split the header
   into TWO rows (CSS Grid, back-to-store/edit-measurements each in their own
   row/track) specifically so their full text labels never had to truncate.
   That is a real, considered trade-off this reverses on explicit, repeated
   instruction for "a clean, compact layout ... tight gap" in ONE row - there
   simply isn't room for "חזרה לחנות" + "עריכת מידות" as full text AND the
   logo AND the cart on one line at ~360-480px, so the nav buttons go
   icon-only here instead (label kept in the DOM + aria-label, just visually
   hidden - screen readers/accessibility name are unaffected). If the 2-row
   full-label layout is ever preferred again, that's a one-line revert of
   this block, not a rebuild. ════════════════════════════════════════════ */
/* ── COMPACT VIEWPORT (≤600px): retune the corner-control tokens ──────────
   Scaling the two glass switches down here is what buys the header row back
   the ~30px it needs to seat five controls plus the brand lockup on a 320-414px
   display. Done by re-declaring the TOKENS rather than by transform:scale():
     · the reserved gutters recompute themselves from the same numbers, so the
       clearance stays exact instead of drifting out of sync with the shrink;
     · transform:scale() would leave the switches' drag physics reading pointer
       deltas in scaled pixels while writing --thumb-x in unscaled ones, making
       the thumb track ~10% fast under the finger. Real geometry, not a visual
       shrink, keeps the 1:1 tracking honest.
   i18n.js / app.js measure the live thumb width, so both switches' snap points
   follow these tokens automatically (see the .lang-switch__thumb note above). */
@media (max-width: 600px) {
  :root {
    --corner-inset: 10px;
    --corner-clear:  8px;
    /* THE HEIGHT SHRINKS AS ONE NUMBER HERE TOO. This block already retuned the
       avatar to 34px and the switch segments to 24px, but --hdr-control-h stayed
       at its desktop 40px - so below 600px the row seated a 34px avatar beside
       40px switches, a 40px edit pill and a 40px cart. That is the original
       ragged-row bug surviving inside the one breakpoint the token did not
       reach, and it is why the avatar in particular still read as offset.
       --profile-size is NOT re-declared: at :root it already derives from
       --hdr-control-h, so retuning the height here carries it automatically. */
    --hdr-control-h: 34px;
    --lang-seg-w:   30px;
    --fs-seg-w:     28px;
    --switch-pad:    3px;
  }
  .lang-switch__seg { height: 24px; font-size: .68rem; }
  .fs-switch__seg   { height: 24px; }
  .fs-switch__icon  { width: 14px; height: 14px; }

  /* ── Touch targets ───────────────────────────────────────────────────────
     Every header control clears 44×44px of TAPPABLE area while keeping its
     compact visual footprint, via a transparent pseudo-element rather than a
     bigger box. Two things make that safe rather than a fresh source of
     invisible overlap:
       · the expansion is symmetric and capped at 5px, and the gaps between
         neighbouring controls are held at ≥12px below (.app-header__nav /
         .app-header__actions), so no two hit areas can ever intersect;
       · the header's own content row is ~44px tall here, so the vertical
         growth stays INSIDE the header band and cannot reach down over the
         page content beneath it and steal its clicks.
     The pseudo-element belongs to the control itself, so a tap in the expanded
     ring is a tap on that control - no pointer-events juggling between
     siblings, which is what made the old overlap unrecoverable. */
  .cart-btn::after,
  .btn-edit-measurements::after,
  .profile-btn::after,
  .lang-switch::after,
  .fs-switch::after {
    content: ""; position: absolute; inset: -6px;
    border-radius: inherit;
  }
  /* -6px, not -5px: border-radius:inherit makes the ring on the round 34px
     controls a CIRCLE, whose usable extent at the extremes falls just inside
     the nominal 44px - probing the rendered page measured only 42×43px of real
     tappable area at -5px. -6px puts the MEASURED figure at 44px+, which is
     what the guideline actually asks for. */
  /* The two switches are wide enough already (56-68px) - they only need
     vertical reach, and must NOT grow sideways into the header content they
     sit beside. -7px because their pill radius costs the same ~1px per side at
     the extremes that the circles do: measured, -6px yielded only 42px. */
  .lang-switch::after,
  .fs-switch::after { inset: -7px 0; }
  /* The edit control is a wide pill, not a circle - it has width to spare and
     needs only the same extra vertical px (measured 43px at -6px). */
  .btn-edit-measurements::after { inset: -7px -6px; }

  /* Each control owns an isolated bounding box + its own stacking slot inside
     the header, so hit-testing resolves to exactly one of them. The lockup is
     purely decorative, so it opts out of pointer-events entirely rather than
     sitting in the tab/hit order between two real controls. */
  .cart-btn, .btn-edit-measurements, .fs-switch { position: relative; z-index: 1; }
  .app-header__brand-lockup { position: relative; z-index: 0; pointer-events: none; }

  /* ── The second overlap: nav vs. the brand lockup ────────────────────────
     The two end zones carry flex:1 1 0 (see .app-header__nav/__actions), which
     forces them to EQUAL widths so the lockup optically centres. That is right
     on desktop, where there is slack to spare - but the zones are lopsided
     (nav holds two controls at ~110px, actions holds one at ~34px), and their
     children are flex-shrink:0, so once the row is tight the nav simply cannot
     fit the half-width it is being handed. It overflowed its share and the edit
     button rendered straight over the wordmark - 30×27px of overlap at 390px.
     On phones the zones therefore size to their CONTENT and the lockup takes
     the remainder, which is both what makes the row fit and what keeps the
     logo centred between the two clusters rather than centred on a viewport
     the clusters are not symmetric about. */
  .app-header__nav,
  .app-header__actions { flex: 0 0 auto; }
  .app-header__brand-lockup { flex: 1 1 auto; justify-content: center; }
}

@media (max-width: 480px) {
  /* No display:grid override here anymore - .app-header stays the ordinary
     flex row from the base rule (line ~533: display:flex;
     justify-content:space-between), and .app-header__nav/.app-header__actions
     stay the ordinary flex zones from the general rule further up (~line
     2613) instead of being flattened to display:contents for grid placement.
     That's what puts back-to-store and edit-measurements back side by side
     in ONE row instead of stacked in two. */
  /* NOTE: longhands, not the `padding` shorthand this used to be - the
     shorthand reset padding-left/right to a flat 10px and silently wiped out
     the corner reservation set on .app-header above, putting the language
     switch straight back on top of the cart button. */
  .app-header {
    --hdr-pad-x: 10px;
    padding-top: calc(6px + var(--safe-t)); padding-bottom: 6px;
    padding-left: var(--hdr-gutter-l); padding-right: var(--hdr-gutter-r);
    gap: 8px;
  }
  .app-header.is-scrolled { padding-top: calc(5px + var(--safe-t)); padding-bottom: 5px; }

  /* 12px, not the old 6px: the edit button's and the fullscreen switch's 44px
     hit rings (±6px each, see above) would otherwise overlap and one would
     start eating the other's taps - the exact class of bug this pass is
     removing. 12px is the tightest gap at which they can only ever abut. */
  .app-header__nav { gap: 12px; }
  .app-header__actions { gap: 12px; }

  /* Icon-only: hide the label, keep the icon + the button's own aria-label
     (accessible name is unaffected - only the VISIBLE text is hidden). The
     fullscreen switch (.fs-switch) is icon-only by design already - no label
     span to hide, no size override needed here. */
  /* 34px, not 32px: paired with the ±5px hit ring above this lands the tap
     target on exactly 44px - the accessibility floor - while the visible pill
     grows by only 2px. */
  .btn-edit-measurements {
    padding: 7px; min-width: 34px; min-height: 34px; justify-content: center;
  }
  .btn-edit-measurements span { display: none; }

  /* PROMINENCE FIX: was 16px/20px (logo) - now that the nav buttons are icon-only
     (freeing up real width in this row), scale both up so PEAR + the mark read as
     the header's visual anchor instead of sitting small next to the icon buttons. */
  .app-header__brand { font-size: 21px; }
  .app-header.is-scrolled .app-header__brand { font-size: 18px; }
  .app-header__sub { display: none; }
  .brand-mark { height: 27px; }
  .app-header.is-scrolled .brand-mark { height: 23px; }
  /* 8px, not the base 10px - buys the wordmark the couple of pixels it needs to
     render in full at 390px instead of ellipsising to "PEA…". */
  .app-header__brand-lockup { gap: 8px; }

  /* .cart-btn's hardcoded 34x34 removed - it now follows --hdr-control-h like
     every other control, which the ≤600px block above already set to 34px. The
     literal was both redundant and a second place for the size to drift. */
}

/* ── NARROW PHONE (≤389px: iPhone SE / 13 mini / older Androids) ──────────
   Five controls, the pear mark AND the wordmark genuinely do not fit one row
   below ~390px - that is the width at which the row overflowed and the edit
   button rendered on top of the brand lockup (measured: 34×27px at 320px).
   The wordmark is the only purely decorative item in the row, so it is what
   gives way rather than an actionable control: "PEAR" drops and the pear MARK
   stays, still centred between the two control clusters. The threshold is the
   measured fit point, not a round number - 390px (iPhone 12/13/14/15, by far
   the most common phone width) keeps the complete lockup. */
@media (max-width: 389px) {
  /* --corner-clear GROWS here rather than tightening, and the 12px
     inter-control gap does not move: both are what keep the 44px hit rings
     from intersecting, so they are correctness constraints, not spacing
     preferences. Tried 6px - the cart's ring then reached back to the language
     switch's edge and, since the switch carries z-index:100 against the
     header's controls, the switch silently won those pixels back (measured at
     320px: the cart fell to 40px of tappable width, then 42px at 8px of
     clearance). 10px is the first value that leaves the full 44px intact.
     The width that pays for it comes from the fullscreen switch, which is
     icon-only and reads perfectly well a touch narrower. */
  :root { --corner-clear: 10px; --fs-seg-w: 24px; }
  .app-header { gap: 6px; }
  .app-header__brand { display: none; }
  .brand-mark { height: 24px; }
  .app-header.is-scrolled .brand-mark { height: 21px; }
}

/* Profile icon: the avatar's size/offset now come from --profile-size /
   --corner-inset (retuned in the ≤600px block above) so the header gutter that
   clears it and the button itself can never disagree. Only what is genuinely
   local to this breakpoint stays here. */
@media (max-width: 480px) {
  .profile-btn__initial { font-size: .78rem; }
  /* NOTCH FIX: was a flat top:50px. The avatar it hangs from is offset by
     var(--safe-t) (the header's own top padding is too), so on a notched phone
     - safe-area-inset-top ~47px - this panel opened ~43px HIGHER than its own
     button and landed on top of the header. Its is-scrolled counterpart
     (body.is-scrolled .profile-dropdown) already added --safe-t; this is the
     unscrolled case catching up. */
  .profile-dropdown { top: calc(50px + var(--safe-t)); right: calc(var(--safe-r) + var(--corner-inset)); }

  /* Same focus-bar vertical-centering the >480px rule does (see body.focus-
     mode:not(.is-scrolled) .profile-btn above), recomputed for the smaller
     34px avatar: bar center is ~34px (22px padding + ~24.6px row / 2);
     avatar center = top + 17 → top:17px lands it there. */
  body.focus-mode:not(.is-scrolled) .profile-btn { top: calc(var(--safe-t) + 17px); }
}

/* Focus bar (garment-name status strip): reserve room on the physical right
   so its text/badge never render under the fixed, always-visible profile
   button. The garment name truncates to a single line with ellipsis instead
   of overflowing or wrapping. Same reservation now also applied at the
   481-768px range below (previously only ≤480px had it - the profile button
   is 40px + a 16px offset there, i.e. wider than the 34px it shrinks to once
   scrolled, so the gutter has to clear the LARGER, pre-scroll size). This is
   the UNSCROLLED gutter only - see the is-scrolled rule below for why it
   drops to a true 0 instead of just shrinking further. */
@media (max-width: 480px) {
  /* padding-right deliberately NOT set here: the ≤1339px rule below is later in
     source at equal specificity, so it always won this longhand anyway - the
     52px here was dead code. Both gutters now come from the shared tokens
     there, which is also what fixes this bar at 320px, where the old fixed
     136px + 68px reservation left only ~116px for the garment name itself. */
  .focus-bar { font-size: 12px; flex-wrap: nowrap; }
  .focus-bar__text {
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
}

/* EDGE-ALIGNED, ALWAYS (was: centered when idle, edge-aligned only once scrolled).
   That toggle is GONE - it was the actual bug. Centering the dot+text+badge group
   used .focus-bar's own padding-right (60px, reserved for the profile button) as
   one wall of the centering box, while the LEFT side only had the base ~16-20px
   padding - so "centered" meant centered inside an ASYMMETRIC box, which reads as
   visually wrong (neither centered nor edge-aligned) regardless of scroll state.
   That's what looked broken in every screenshot, not just the un-scrolled one.
   Fix: flex-start (RTL = visual right, alongside the badge) unconditionally.
   Range is <=1339px, not <=768px: this used to stop at the old mobile
   breakpoint, leaving 769-1339px (a very ordinary desktop/laptop width) with
   NO gutter at all - the unscrolled avatar (40px + 16px inset = 56px) sat
   directly on top of the dot/text there, nothing pushing them apart, since
   the >=1340px ambient-rail gutter below hasn't kicked in yet either. */
@media (max-width: 1339px) {
  .focus-bar {
    justify-content: flex-start;   /* RTL flex-start = visual right, alongside the badge */
    /* Avatar clearance + breathing gap, from the same tokens the header
       reserves against (see TOP-CORNER CONTROL GEOMETRY) - so this bar and the
       header always agree on where the fixed corner controls actually are. */
    padding-right: var(--hdr-gutter-r);
    /* BUG FIX: physical-LEFT mirror of the padding-right reservation above -
       was missing entirely, so .focus-bar__badge (margin-inline-start:auto
       pushes it flush to the row's inline-end edge, which is the physical
       LEFT in RTL) rendered directly under .lang-switch (fixed
       top:16px;left:16px;z-index:100 - always the physical left corner,
       regardless of dir). ~92px actual switch width (two-segment glass pill,
       "EN"/"עב") + 16px offset + a ~28px breathing gap = 136px. Unlike
       .profile-btn, the language switch never fades/shrinks on scroll (see
       .lang-switch - no .is-scrolled variant exists for it at all), so
       this reservation is intentionally NOT relaxed anywhere padding-right
       is below (.focus-bar.is-scrolled only ever touches padding-right).
       Was a hardcoded 136px derived from the desktop switch size; now the
       shared token, so it tracks the switch down to 68px on phones instead of
       over-reserving half a 320px bar. */
    padding-left: var(--hdr-gutter-l);
  }
}

/* Once scrolled, the avatar fades out (see body.focus-mode.is-scrolled
   .profile-btn above) instead of sharing the edge - so the gutter it used to
   reserve (mobile's 60px above, or the ≥1340px ambient-rail gutter below) is
   no longer needed. A literal 0 read as clipped/cramped in practice (the dot
   sitting flush on the physical screen edge, no breathing room) - 14px is
   the smallest edge inset already used elsewhere in this file (the ≤480px
   profile-btn), so it stays visually "flush" without touching the glass.
   Deliberately UNSCOPED by width - a compound class selector (0,2,0) beats a
   plain `.focus-bar` rule (0,1,0) regardless of which media query it's in or
   its source order, so this wins at every breakpoint, including inside
   `@media (min-width:1340px)` above. That's intentional: the rail-collision
   bug that gutter guards against only ever hit the STATIC, unscrolled bar -
   the scrolled bar is a distinct compact-chrome state, sitting above the
   rails with its own frosted/blurred background.
   ARRIVING direction gets its OWN transition (a CSS transition's timing is
   taken from the after-change style, so base ≠ is-scrolled can genuinely
   differ): a small spring overshoot, distinct from the calm ease-out on the
   way back (base rule above) - the classic "confident snap in, gentle settle
   out" asymmetry premium UI uses instead of one flat curve both ways. */
.focus-bar.is-scrolled { padding-right: 14px; transition: padding .55s var(--spring); }

/* Mirror of the header's reclaim above: .focus-bar is sticky too, so once the
   corner controls have scrolled off, its left reservation is dead space. Keyed
   to corners-cleared rather than is-scrolled for the same reason - is-scrolled
   fires while they are still half visible. 14px matches the flush inset the
   scrolled bar already uses on its right. */
body.corners-cleared .focus-bar { padding-left: 14px; }

/* ════════════════════════════════════════════════════════════════════════
   ✦ CAMERA - flip control · front-only mirror · orientation-adaptive stage
   ════════════════════════════════════════════════════════════════════════ */

/* Flip front/rear - floating glass icon pinned to the free (inline-end) corner,
   opposite the LIVE badge and the swatch rail. Shown only in the live preview;
   hidden the moment a billed session / result / clip takes over the stage. */
.camera-card__flip {
  position: absolute; top: 14px; inset-inline-end: 14px; z-index: 7;
  display: none; align-items: center; justify-content: center;
  width: 40px; height: 40px; border-radius: 50%;
  border: 1px solid var(--glass-border);
  background: rgba(255,255,255,.85);
  -webkit-backdrop-filter: blur(10px) saturate(160%); backdrop-filter: blur(10px) saturate(160%);
  color: var(--ink); cursor: pointer;
  box-shadow: var(--shadow-soft), inset 0 1px 0 rgba(255,255,255,.6);
  transition: transform .35s var(--ease), background .3s var(--ease), box-shadow .3s var(--ease);
}
.camera-card.live .camera-card__flip { display: inline-flex; }
.camera-card.show-live  .camera-card__flip,
.camera-card.show-result .camera-card__flip,
.camera-card.show-clip  .camera-card__flip { display: none; }
.camera-card__flip:hover { transform: rotate(-20deg); background: #fff; box-shadow: var(--shadow); }
.camera-card__flip:active { transform: scale(.9) rotate(-180deg); }
.camera-card__flip:focus-visible { outline: 2px solid var(--pear); outline-offset: 2px; }
.camera-card__flip[disabled] { opacity: .5; cursor: default; }

/* Selfie mirror is for the FRONT camera only. On the rear ("environment") camera the
   preview must read true so background text isn't reversed. (JS sets data-facing.) */
.camera-card[data-facing="environment"] #webcam { transform: none; }

/* Orientation-adaptive stage (display only - Decart still receives 512×288). Once the
   real stream reports its shape, the card follows it so the preview isn't over-cropped:
   portrait on a phone held upright, wider when the feed is landscape. */
.camera-card[data-orientation="portrait"]  { aspect-ratio: 3 / 4; }
.camera-card[data-orientation="landscape"] { aspect-ratio: 4 / 3; }

/* ════════════════════════════════════════════════════════════════════════
   ✦ LIQUID GLASS DESIGN SYSTEM (2026-09-18)
   ────────────────────────────────────────────────────────────────────────
   One layer, appended LAST on purpose: every rule below restates a selector that
   already exists above (or is new), so it wins on source order at equal
   specificity without editing forty scattered rules - and deleting this block
   restores the previous look exactly. It touches paint only (fill, border,
   blur, shadow, colour, motion); it never sets width/height/padding/position on
   an existing desktop component, so the breakpoint geometry above cannot drift.
   The one deliberate exception is §7, which turns the two overlays into bottom
   sheets at ≤768px.

     1. tokens            5. step transitions (identity → OTP → size form)
     2. unified surfaces  6. terms & accuracy consent panel
     3. readable inputs   7. mobile sheets (.drag-indicator + sheet-gestures.js)
     4. gloss CTAs        8. cookie / local-storage banner (cookie-consent.js)
   ════════════════════════════════════════════════════════════════════════ */

/* ── 1 · tokens ─────────────────────────────────────────────────────────── */
:root {
  --lg-blur:         blur(20px) saturate(180%);
  --lg-border:       1px solid rgba(255, 255, 255, .4);
  --lg-fill:         linear-gradient(135deg, rgba(255, 255, 255, .58) 0%, rgba(255, 255, 255, .24) 100%);
  /* Multi-layered edge: a soft top refraction line, a hairline of shade along the
     bottom lip, and a faint full inner ring - the SAME inner gloss the buttons use
     (--lgb-shadow below), so cards and controls read as one material. */
  --lg-edge:         inset 0 1px 1px 0 rgba(255, 255, 255, .6),
                     inset 0 -1px 1px 0 rgba(0, 0, 0, .06),
                     inset 0 0 0 1px rgba(255, 255, 255, .14);
  --lg-edge-dark:    inset 0 1px 0 rgba(255, 255, 255, .16),
                     inset 0 0 0 1px rgba(255, 255, 255, .05);
  --lg-shadow:       0 0 0 .5px rgba(15, 23, 42, .05),
                     0 2px 6px rgba(15, 23, 42, .04),
                     0 12px 32px rgba(15, 23, 42, .08),
                     0 32px 72px rgba(15, 23, 42, .10);

  /* Input legibility. Slate-900 / gray-900 on white glass is ~17:1; the old
     --muted-2 placeholder (#9a9aa0) was ~2.6:1 and read as disabled text. */
  --ink-input:       #0F172A;
  --ink-label:       #111827;
  --ink-placeholder: rgba(15, 23, 42, .5);
  --ink-secondary:   #4b5563;

  /* "primary" = the pear green the rings/tints above were already built from. */
  --primary-rgb:     141, 182, 0;
  --primary-solid:   rgb(112, 148, 0);   /* checked fills - white glyph on it ≥ 3:1 */

  /* iOS sheet curve - fast departure, long soft settle, no overshoot. */
  --ease-sheet:      cubic-bezier(.32, .72, 0, 1);
  /* Step slide distance. Forward motion follows the reading direction: next step
     arrives from the right in LTR, from the left in RTL. */
  --step-shift:      32px;
  /* +1 in LTR, -1 in RTL. Multiplied into directional motion (the progress sheen) so
     "forward" runs toward the end of the line in either language. */
  --dir-x:           1;

  /* ── shared glass material (cards, badges, progress, checkboxes, banner) ──
     Buttons do NOT read these: §4 is a self-contained paint-only overlay with its own
     literal values, so the button baseline cannot drift when a token here changes.
     --font-ui is the TEXT face (SF Pro on Apple, Heebo elsewhere); buttons keep their
     baseline faces - see §4 and the fonts request in index.html. */
  --font-ui:           -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Heebo", "Assistant", sans-serif;
  --ease-spring:       cubic-bezier(.2, .8, .2, 1);
  --lgb-blur:          blur(24px) saturate(200%);
  --lgb-blur-modal:    blur(30px) saturate(180%);   /* modal panels, sheets and their scrims */
  --lgb-border:        rgba(255, 255, 255, .45);
  --lgb-glass:         linear-gradient(135deg, rgba(255, 255, 255, .4) 0%, rgba(255, 255, 255, .1) 100%);
  --lgb-shadow:        0 12px 32px -8px rgba(0, 0, 0, .12),
                       inset 0 1px 1px 0 rgba(255, 255, 255, .7),
                       inset 0 -1px 1px 0 rgba(0, 0, 0, .1);
  /* The translucent sheen: bright at the top lip, clearing toward the middle. */
  --lgb-sheen:         linear-gradient(180deg, rgba(255, 255, 255, .5) 0%, rgba(255, 255, 255, .12) 52%, rgba(255, 255, 255, .04) 100%);
  /* FOCUS is emerald-500, deliberately NOT the pear accent: focus is "where the
     keyboard is", selection/brand is "what is chosen" - two meanings, two hues, so a
     focused-and-checked control never reads as doubly checked. */
  --focus-rgb:         16, 185, 129;
}
html[dir="rtl"] { --step-shift: -32px; --dir-x: -1; }

/* ── 2 · unified surfaces ───────────────────────────────────────────────────
   Modal cards, banners, popovers and the toast share one blur, one border and one
   edge/shadow stack. FILLS stay per-surface on purpose: the dropdowns and help
   popover sit over the live camera and keep their near-opaque fills (see the
   "opaque enough that NOTHING behind it can read through" note on .cart-dropdown);
   only the glass treatment is unified, not their legibility floor. */
/* MODAL CONTAINERS - the Screen-1 card (it IS the modal's content inside the widget
   iframe), Compare, garment-detect: 28px corners and the 30px modal blur. On phones
   §7 turns the two overlays into bottom sheets with 28px top corners. */
#screen-calculator .container {
  background: var(--lg-fill);
  -webkit-backdrop-filter: var(--lgb-blur-modal);
          backdrop-filter: var(--lgb-blur-modal);
  border: var(--lg-border);
  border-radius: 28px;
  box-shadow: var(--lg-shadow), var(--lg-edge);
}
.pcmp__panel,
.gd-overlay__panel {
  -webkit-backdrop-filter: var(--lgb-blur-modal);
          backdrop-filter: var(--lgb-blur-modal);
  border: var(--lg-border);
  border-radius: 28px;
  box-shadow: var(--lg-shadow), var(--lg-edge);
}
/* ...and their scrims frost at the same strength */
.pcmp__backdrop,
.gd-overlay__backdrop {
  -webkit-backdrop-filter: var(--lgb-blur-modal);
          backdrop-filter: var(--lgb-blur-modal);
}
/* THE BUG THIS CLOSES: "the modal blur is declared but nothing behind it is blurred."
   ─────────────────────────────────────────────────────────────────────────────
   Measured 2026-09-18 on the Compare modal: computed backdrop-filter blur(30px) on
   both scrim and panel, yet the header underneath rendered pixel-sharp (edge energy
   1.55 vs 0.05 when blurred; a bare control page proved headless Chromium blurs fine).
   Cause: `.screen.active` and `.pear-compare-overlay.show` both run `fade ... both`,
   and fill-mode BOTH keeps each animation alive after it ends. Chromium makes an
   element with a live opacity animation a backdrop root, so the scrim and panel could
   only ever sample their own (empty) layer. Bisected: removing either fill alone is not
   enough - both must go.
   The same filled animation also left the screen's identity transform in place, which
   makes #screen-fitting the containing block for `position: fixed` - so the Compare
   "modal" was fixed to the whole PAGE height, not the viewport, and sat half off-screen
   on a tall page (the .profile-btn note records the same side effect).
   Fix: `backwards` instead of `both`. `fade` ends on opacity:1 / transform:none - the
   elements' own resting values - so nothing visible changes except that the effect
   ends when the animation does. The Compare overlay is the only position:fixed element
   inside a screen (the lightbox, detect overlay, toasts and banner live on <body>).
   The Screen-1 card had the same problem one level down (.calc-shell), fixed with it. */
.screen.active,
.screen.active .calc-shell,
.pear-compare-overlay.show { animation-fill-mode: backwards; }
/* popovers and dropdowns: same material, the lighter card blur */
.profile-dropdown,
.cart-dropdown,
.pear-help-widget__popover {
  -webkit-backdrop-filter: var(--lg-blur);
          backdrop-filter: var(--lg-blur);
  border: var(--lg-border);
  box-shadow: var(--lg-shadow), var(--lg-edge);
}
.toast {
  -webkit-backdrop-filter: var(--lg-blur);
          backdrop-filter: var(--lg-blur);
  border: var(--lg-border);
  box-shadow: var(--lg-shadow), var(--lg-edge-dark);
}
/* Banners INSIDE the Screen-1 card get the border + edge but NO blur: the card is a
   backdrop root (see "ONE blur pass per card" above), so a blur here would sample
   only the card's own smooth gradient and paint nothing but compositing cost. */
#screen-calculator .focus-calc-hint,
.measurements-refresh-notice {
  border: var(--lg-border);
  box-shadow: var(--lg-edge), 0 4px 14px rgba(15, 23, 42, .05);
}
/* Badges share the button material (§4): the glossy .35 frame, the inner gloss, the
   24px blur - fills unchanged, so LIVE stays legible over the camera. The kicker is
   inside the Screen-1 card on one screen (no blur there, same reason as above) and on
   the page itself on #screen-locked, so it gets the frame + gloss and keeps its own
   blur rule. */
.camera-card__live,
.focus-bar__badge {
  border: 1px solid var(--lgb-border);
  -webkit-backdrop-filter: var(--lgb-blur);
          backdrop-filter: var(--lgb-blur);
  box-shadow: var(--lgb-shadow);
  font-family: var(--font-ui);
}
.focus-bar__badge { background: var(--lgb-sheen), var(--pear-tint); }
/* HEADER BADGE ("PEAR · PERSONAL FIT ENGINE") - an ELEVATED glass pill: the button
   body (sheen + glass) over a white wash, a lifted two-stage drop shadow, and a looser
   .08em track (was .16em - at that width the pill read as a banner, not a badge). */
.kicker {
  font-family: var(--font-ui);
  font-weight: 700;
  letter-spacing: .08em;
  background: var(--lgb-sheen), var(--lgb-glass), linear-gradient(180deg, rgba(255, 255, 255, .56), rgba(255, 255, 255, .24));
  border: 1px solid var(--lgb-border);
  box-shadow: 0 10px 24px -10px rgba(0, 0, 0, .18), 0 2px 6px -2px rgba(0, 0, 0, .06),
              inset 0 1px 1px 0 rgba(255, 255, 255, .8), inset 0 -1px 1px 0 rgba(0, 0, 0, .06);
}
/* its twin on the dark "already measured" panel (#screen-locked) */
.calc-editorial__kicker {
  letter-spacing: .08em;
  background: linear-gradient(180deg, rgba(255, 255, 255, .2) 0%, rgba(255, 255, 255, .05) 52%, rgba(255, 255, 255, 0) 100%),
              linear-gradient(135deg, rgba(255, 255, 255, .16), rgba(255, 255, 255, .04));
  border: 1px solid rgba(255, 255, 255, .35);
  box-shadow: 0 10px 24px -10px rgba(0, 0, 0, .5), inset 0 1px 1px 0 rgba(255, 255, 255, .3), inset 0 -1px 1px 0 rgba(0, 0, 0, .3);
}

/* PROGRESS ("שלב 1 מתוך 2" · %) - a glass channel with a glowing liquid bar floating
   inside it. The 2px padding is what makes the bar "float": the fill sits inset from
   the frosted rim instead of touching it. Width eases on the spring curve, so the jump
   to 100% when a size resolves reads as fill, not as a snap.
   DIRECTION: the bar grows from the inline-START edge (right in Hebrew), so its bright
   leading edge and the travelling sheen both run toward inline-END - the gradient is
   mirrored for RTL, and progShine is re-keyed on --dir-x below. */
.step-progress-label {
  font-weight: 600; font-size: .8rem; letter-spacing: .01em;
  color: var(--ink-secondary);
}
.step-progress-label span:last-child { color: #3d5200; font-weight: 800; font-variant-numeric: tabular-nums; }
.step-progress-track {
  height: 12px; padding: 2px;
  background: var(--lgb-sheen), linear-gradient(180deg, rgba(255, 255, 255, .5), rgba(255, 255, 255, .22));
  border: 1px solid var(--lgb-border);
  -webkit-backdrop-filter: var(--lgb-blur);
          backdrop-filter: var(--lgb-blur);
  box-shadow: inset 0 1px 2px rgba(15, 23, 42, .12), inset 0 -1px 1px rgba(255, 255, 255, .7),
              0 6px 16px -8px rgba(0, 0, 0, .14);
}
.step-progress-fill {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .55) 0%, rgba(255, 255, 255, 0) 60%),
    linear-gradient(90deg, rgb(104, 138, 0), rgb(160, 200, 22));
  box-shadow: 0 0 12px rgba(var(--primary-rgb), .55), 0 0 2px rgba(var(--primary-rgb), .8);
  transition: width .8s var(--ease-spring);
}
html[dir="rtl"] .step-progress-fill {
  background:
    linear-gradient(180deg, rgba(255, 255, 255, .55) 0%, rgba(255, 255, 255, 0) 60%),
    linear-gradient(270deg, rgb(104, 138, 0), rgb(160, 200, 22));
}
@keyframes progShine {
  from { transform: translateX(calc(-100% * var(--dir-x))); }
  to   { transform: translateX(calc(240% * var(--dir-x))); }
}

/* ── 3 · readable inputs ─────────────────────────────────────────────────── */
.form-group label,
.form-group__label { color: var(--ink-label); }
.optional { color: #5b6472; }   /* was --muted-2 (~2.6:1) - an unreadable "(optional)" is a hidden instruction */

.form-group input,
.form-group select,
#otpInput {
  color: var(--ink-input);
  caret-color: var(--ink-input);
  font-weight: 500;
  /* Frosted body on the buttons' 135deg axis. No backdrop-filter: every input lives
     inside the Screen-1 card, which is a backdrop root (the PERF rule). */
  background: linear-gradient(135deg, rgba(255, 255, 255, .74) 0%, rgba(255, 255, 255, .44) 100%);
  /* The dark hairline stays: a .35-white frame alone vanishes on a white card, and an
     input you cannot find the edge of is worse than one that is not glass. The glass
     reads through the inner gloss instead - the buttons' own top lip and bottom shade. */
  border: 1px solid rgba(15, 23, 42, .14);
  box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, .6),
              inset 0 -1px 1px 0 rgba(0, 0, 0, .05),
              inset 0 1px 3px rgba(15, 23, 42, .06);
  transition: border-color .3s var(--ease), box-shadow .35s var(--ease),
              background .3s var(--ease), transform .35s var(--ease-sheet);
}
#otpInput { font-weight: 800; }   /* the code itself keeps its display weight */
.form-group input,
.form-group select { font-family: var(--font-ui); }   /* values match the buttons; #otpInput keeps its display face */
.form-group input::placeholder,
#otpInput::placeholder {
  color: var(--ink-placeholder);
  opacity: 1;   /* Firefox dims placeholders by default - the token is the final colour */
}
.form-group input:hover,
.form-group select:hover,
#otpInput:hover {
  border-color: rgba(var(--focus-rgb), .32);
  background: linear-gradient(135deg, rgba(255, 255, 255, .86) 0%, rgba(255, 255, 255, .58) 100%);
}
.form-group input:focus,
.form-group select:focus,
#otpInput:focus {
  outline: none;
  border-color: rgba(var(--focus-rgb), .62);   /* the smooth border highlight */
  background: rgba(255, 255, 255, .94);
  box-shadow:
    0 0 0 2px rgba(var(--focus-rgb), .30),   /* ring-2 ring-emerald-500/30 */
    0 0 24px rgba(var(--focus-rgb), .16),    /* ambient glass glow */
    inset 0 2px 5px rgba(15, 23, 42, .07);   /* smooth inner shadow */
  transform: scale(1.005);
}
/* Autofill paints its own opaque background + text colour over the glass. Keep the
   ink token and hold the UA background off indefinitely (the long-transition trick -
   Chromium/WebKit expose no property to override it directly). */
.form-group input:-webkit-autofill,
#otpInput:-webkit-autofill {
  -webkit-text-fill-color: var(--ink-input);
  transition: background-color 600000s 0s, color 600000s 0s;
}
/* iOS zooms the page on focus for any input under 16px - the old .98rem (15.7px) did
   exactly that on every tap into height/weight, and never zoomed back out. */
@media (max-width: 768px) {
  .form-group input,
  .form-group select { font-size: 16px; }
}

/* ── 4 · Liquid Glass on every button - a NON-DESTRUCTIVE overlay ─────────────
   HISTORY. Two earlier passes restyled buttons by REPLACING their rules (a shared
   recipe that set font, weight, tracking, fill and border on every button). Measured
   against the baseline (1912c0b - byte-identical to 7564c14 for buttons), 36 of 53
   button boxes changed: +3-10px of height from the swapped face and weight, and a
   real 1px border added to two border-less buttons. Those passes are gone; every
   button's own rules (layout, face, fill, states) are the baseline again.

   THIS LAYER MAY ONLY PAINT. It sets nothing that can move a pixel of layout:
   no width, height, padding, margin, border-WIDTH, radius, font, line-height,
   letter-spacing, display, gap or alignment. What it adds:
     · backdrop-filter  blur(20px) saturate(180%)
     · border-COLOR     rgba(255,255,255,.4) - a colour change only, so a button that
                        has no border gets none; the two border-less ones (help close,
                        log out) get the same line as an inset ring instead
     · box-shadow       0 8px 24px -6px + a 1px inner top light
     · ::before         the 135deg glass sheen, layered OVER each button's own fill,
                        so primary stays pear, Stop stays dark, Add-to-Cart's success
                        fill stays green - the overlay adapts to whatever is under it
     · physics          hover scale(1.02) + the sheen brightening; press scale(.97);
                        both on cubic-bezier(.2,.8,.2,1)

   SPECIFICITY IS THE CONTRACT. The paint rule is `body :is(...)` = (0,1,1): it beats
   every button's ONE-class base rule - including the size chips and replay buttons,
   whose styles app.js injects AFTER this sheet - and loses to every TWO-class state
   rule: :disabled, :hover fills, .is-live, .is-active, .is-oos, .is-done. That is what
   keeps states exactly as they were. The consent-locked Continue is the one state
   this layer draws itself (it did not exist at baseline).
   Blur inside the Screen-1 card stays off - the id-level PERF rule ("ONE blur pass per
   card") outranks this and turns child blur off there, where it painted nothing.

   Out of scope on purpose: the segmented toggles (language, fullscreen, gender) and
   the detect-overlay tabs position their thumbs from live geometry and are already
   glass at baseline; colour swatches and catalog/gallery tiles are content, not
   buttons. The list below is repeated verbatim in each rule - keep them in step:

     .btn-primary .btn-ghost .btn-capture .btn-add-cart .btn-watch .btn-download
     .btn-update-measure .btn-edit-measurements .cart-btn .cookie-btn .pear-compare-bar
     .plb-btn .pear-sz-btn .pear-rz-btn .pcmp__close .terms-modal__close .gd-close .pear-lightbox__close
     .pear-help-widget__btn .pear-help-widget__close .camera-card__flip .cl-arrow
     .pip-retake .pip-clear .profile-btn .profile-dropdown__logout .cart-line__remove */
body :is(.btn-primary, .btn-ghost, .btn-capture, .btn-add-cart, .btn-watch, .btn-download,
         .btn-update-measure, .btn-edit-measurements, .cart-btn, .cookie-btn, .pear-compare-bar,
         .plb-btn, .pear-sz-btn, .pear-rz-btn, .pcmp__close, .terms-modal__close, .gd-close, .pear-lightbox__close,
         .pear-help-widget__btn, .pear-help-widget__close, .camera-card__flip, .cl-arrow,
         .pip-retake, .pip-clear, .profile-btn, .profile-dropdown__logout, .cart-line__remove) {
  -webkit-backdrop-filter: blur(20px) saturate(180%);
          backdrop-filter: blur(20px) saturate(180%);
  border-color: rgba(255, 255, 255, .4);
  box-shadow: 0 8px 24px -6px rgba(0, 0, 0, .12), inset 0 1px 1px 0 rgba(255, 255, 255, .6);
  isolation: isolate;                      /* a stacking context, so ::before can sit at -1 */
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;              /* no double-tap-zoom wait on phones */
  /* Every button keeps its OWN transition list (durations, delays, which properties -
     #btn-next-screen's reveal, the avatar's scroll compaction, Add-to-Cart's morph);
     only the easing is unified, which a longhand can do without touching the list. */
  transition-timing-function: cubic-bezier(.2, .8, .2, 1);
}
/* the two border-less buttons: the frame as an inset ring, never a real border */
body :is(.pear-help-widget__close, .profile-dropdown__logout) {
  box-shadow: 0 8px 24px -6px rgba(0, 0, 0, .12), inset 0 1px 1px 0 rgba(255, 255, 255, .6),
              inset 0 0 0 1px rgba(255, 255, 255, .4);
}
/* ::before needs a positioned box; these are static at baseline (position:relative
   with no offsets moves nothing). The others are already relative or absolute. */
:is(.btn-ghost, .btn-edit-measurements, .btn-update-measure, .pcmp__close, .terms-modal__close, .gd-close,
    .pear-help-widget__btn, .pear-help-widget__close, .pip-clear, .profile-dropdown__logout) {
  position: relative;
}
/* Three buttons transition only colour at baseline, so a scale would snap - their
   own list restated with transform + opacity added. */
.pear-help-widget__close { transition: background .2s var(--ease), color .2s var(--ease), transform .3s cubic-bezier(.2, .8, .2, 1); }
.profile-dropdown__logout,
.cart-line__remove { transition: background .25s var(--ease), color .25s var(--ease), transform .3s cubic-bezier(.2, .8, .2, 1); }
/* The gender options inherit the page face (font-family: inherit). The page face moved
   to SF/Heebo; pin theirs to the baseline page face so the switch keeps its height. */
#genderToggle .gender-option { font-family: "Inter", system-ui, -apple-system, "Segoe UI", Arial, sans-serif; }

/* the glass sheen - OVER the button's own fill, UNDER its label */
body :is(.btn-primary, .btn-ghost, .btn-capture, .btn-add-cart, .btn-watch, .btn-download,
         .btn-update-measure, .btn-edit-measurements, .cart-btn, .cookie-btn, .pear-compare-bar,
         .plb-btn, .pear-sz-btn, .pear-rz-btn, .pcmp__close, .terms-modal__close, .gd-close, .pear-lightbox__close,
         .pear-help-widget__btn, .pear-help-widget__close, .camera-card__flip, .cl-arrow,
         .pip-retake, .pip-clear, .profile-btn, .profile-dropdown__logout, .cart-line__remove)::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  border-radius: inherit;
  background: linear-gradient(135deg, rgba(255, 255, 255, .25) 0%, rgba(255, 255, 255, .08) 100%);
  opacity: .8;
  transition: opacity .3s cubic-bezier(.2, .8, .2, 1);
}
body :is(.btn-primary, .btn-capture, .btn-add-cart, .btn-ghost, .cookie-btn):disabled::before { opacity: .4; }

/* HOVER - mouse only (touch keeps each button's baseline behaviour). Weight (0,6,0)
   outranks every baseline hover transform, e.g. #btn-next-screen's is restated at id
   weight; .cl-arrow keeps the translateY(-50%) that centres it. */
@media (hover: hover) and (pointer: fine) {
  :is(.btn-primary, .btn-ghost, .btn-capture, .btn-add-cart, .btn-watch, .btn-download,
      .btn-update-measure, .btn-edit-measurements, .cart-btn, .cookie-btn, .pear-compare-bar,
      .plb-btn, .pear-sz-btn, .pear-rz-btn, .pcmp__close, .terms-modal__close, .gd-close, .pear-lightbox__close,
      .pear-help-widget__btn, .pear-help-widget__close, .camera-card__flip,
      .pip-retake, .pip-clear, .profile-btn, .profile-dropdown__logout,
      .cart-line__remove):hover:not(:disabled):not([aria-disabled="true"]):not(.is-done):not(.is-loading) {
    transform: scale(1.02);
  }
  #btn-next-screen:hover:not(:disabled):not([aria-disabled="true"]) { transform: scale(1.02); }
  .cl-arrow:hover { transform: translateY(-50%) scale(1.02); }
  body :is(.btn-primary, .btn-ghost, .btn-capture, .btn-add-cart, .btn-watch, .btn-download,
           .btn-update-measure, .btn-edit-measurements, .cart-btn, .cookie-btn, .pear-compare-bar,
           .plb-btn, .pear-sz-btn, .pear-rz-btn, .pcmp__close, .terms-modal__close, .gd-close, .pear-lightbox__close,
           .pear-help-widget__btn, .pear-help-widget__close, .camera-card__flip, .cl-arrow,
           .pip-retake, .pip-clear, .profile-btn, .profile-dropdown__logout,
           .cart-line__remove):hover:not(:disabled):not([aria-disabled="true"])::before { opacity: 1; }
}

/* PRESS - mouse and touch. After the hover rules at equal weight, so it wins while
   both apply; a short duration only while pressed, so the release keeps each
   button's own (longer) settle. */
:is(.btn-primary, .btn-ghost, .btn-capture, .btn-add-cart, .btn-watch, .btn-download,
    .btn-update-measure, .btn-edit-measurements, .cart-btn, .cookie-btn, .pear-compare-bar,
    .plb-btn, .pear-sz-btn, .pear-rz-btn, .pcmp__close, .terms-modal__close, .gd-close, .pear-lightbox__close,
    .pear-help-widget__btn, .pear-help-widget__close, .camera-card__flip,
    .pip-retake, .pip-clear, .profile-btn, .profile-dropdown__logout,
    .cart-line__remove):active:not(:disabled):not([aria-disabled="true"]):not(.is-done):not(.is-loading) {
  transform: scale(.97);
  transition-duration: .2s;
}
/* #btn-next-screen: its baseline transition delays EVERY property by .18s (for the
   reveal stagger), which also held every press back 180ms - zeroed while pressed. */
#btn-next-screen:active:not(:disabled):not([aria-disabled="true"]) {
  transform: scale(.97);
  transition-duration: .2s;
  transition-delay: 0s;
}
.cl-arrow:active { transform: translateY(-50%) scale(.97); transition-duration: .2s; }

/* Consent lock. calculateSize() owns [disabled] ("is there a size?"); the consent
   gate owns [aria-disabled] (see the COMPLIANCE block in app.js). Drawn as the
   baseline's own disabled look (.btn-primary:disabled), with a slightly darker label
   so the shopper can read what is waiting. */
#btn-next-screen[aria-disabled="true"] {
  background: rgba(255, 255, 255, .3);
  border-color: var(--glass-line);
  color: #6b7280;
  box-shadow: none;
  cursor: not-allowed;
  animation: none;
}
#btn-next-screen[aria-disabled="true"]:hover,
#btn-next-screen[aria-disabled="true"]:active { transform: none; }
#btn-next-screen[aria-disabled="true"]::after { animation: none; }

/* ── 5 · step transitions ───────────────────────────────────────────────────
   Identity → OTP → size form is a display toggle in app.js (showOtpScreen /
   showSizeForm / showIdentityGate), and a CSS animation restarts whenever an
   element goes from display:none to rendered - so each step slides in on its own
   with zero JS timing and no change to the routing code. Enter-only by design: an
   exit animation would need the outgoing step to stay painted after app.js has
   hidden it, i.e. a timer between "hidden" and "gone" that the routing never waits
   for. Outranks `.screen.active form`'s fade on specificity (ids). */
@keyframes pearStepIn {
  from { opacity: 0; transform: translate3d(var(--step-shift), 0, 0); }
  to   { opacity: 1; transform: none; }
}
#screen-calculator.active #identityForm,
#screen-calculator.active #screen-otp,
#screen-calculator.active #sizeForm {
  animation: pearStepIn .56s var(--ease-sheet) both .06s;
}

/* ── 6 · terms & accuracy consent (#consentPanel) + the policy popup (#termsModal) ──
   ONE compact row since 2026-09-19: a checkbox and a sentence whose middle is a link
   that opens the popup. No card and no alert box in the form any more - the accuracy
   disclaimer and the measurement-processing statement live in the popup, one tap
   away, and ticking the box accepts them. Inside the Screen-1 card → no blur. */
.consent-panel { position: relative; margin-top: 14px; text-align: start; }
.result-actions.is-ready .consent-panel { animation: pearPopIn .55s cubic-bezier(.34, 1.56, .64, 1) both .12s; }

.consent-check {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-radius: 14px;
  -webkit-tap-highlight-color: transparent;
  transition: background .25s var(--ease);
}
.consent-check:hover { background: rgba(255, 255, 255, .45); }
.consent-check__control { position: relative; flex: none; width: 22px; height: 22px; }
/* The REAL checkbox, stretched over the drawn box at opacity 0 - so pointer, label,
   keyboard and screen reader all operate the native control (never display:none,
   which would drop it from the tab order and the accessibility tree). */
.consent-check__control input {
  position: absolute; inset: 0; z-index: 2;
  width: 100%; height: 100%; margin: 0;
  opacity: 0; cursor: pointer;
}
.consent-check__box {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  border-radius: 7px;
  /* Glass body (sheen over the 135deg frost). The dark 1.5px stroke stays for the
     same reason the inputs keep theirs: an unticked box must be findable on white. */
  background: var(--lgb-sheen), linear-gradient(135deg, rgba(255, 255, 255, .9), rgba(255, 255, 255, .52));
  border: 1.5px solid rgba(15, 23, 42, .28);
  box-shadow: 0 4px 10px -6px rgba(0, 0, 0, .22),
              inset 0 1px 1px 0 rgba(255, 255, 255, .85), inset 0 -1px 1px 0 rgba(0, 0, 0, .08);
  transition: background .3s var(--ease-sheet), border-color .3s var(--ease),
              box-shadow .3s var(--ease), transform .35s var(--ease-sheet);
}
.consent-check__box svg {
  width: 14px; height: 14px;
  fill: none; stroke: #fff; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round;
  stroke-dasharray: 16; stroke-dashoffset: 16;
  transition: stroke-dashoffset .38s var(--ease-sheet) .04s;
}
.consent-check input:checked + .consent-check__box {
  /* liquid pear: a specular top half over the solid fill, then a one-shot spring pop */
  background: linear-gradient(180deg, rgba(255, 255, 255, .38) 0%, rgba(255, 255, 255, 0) 55%),
              linear-gradient(180deg, rgb(150, 192, 16) 0%, var(--primary-solid) 100%);
  border-color: rgba(84, 112, 0, .9);
  box-shadow: 0 6px 14px -4px rgba(var(--primary-rgb), .5),
              inset 0 1px 1px 0 rgba(255, 255, 255, .6), inset 0 -1px 1px 0 rgba(0, 0, 0, .12);
  animation: pearCheckPop .42s var(--ease-spring);
}
@keyframes pearCheckPop {
  0%   { transform: scale(.86); }
  55%  { transform: scale(1.08); }
  100% { transform: scale(1); }
}
.consent-check input:checked + .consent-check__box svg { stroke-dashoffset: 0; }
.consent-check input:active + .consent-check__box { transform: scale(.9); }
.consent-check input:focus-visible + .consent-check__box {
  box-shadow: 0 0 0 2px #fff, 0 0 0 4px rgba(var(--focus-rgb), .55);
}
/* The sentence. Its two <label for> halves tick the box; the link between them does
   not (it is a <button>, deliberately outside any <label>). It may wrap on a narrow
   phone - it is one row of layout, not a forced single line of text. */
.consent-check__text {
  margin: 0; min-width: 0;
  font-size: .84rem; line-height: 1.45; font-weight: 500;
  color: var(--ink-label);
}
.consent-check__text label { cursor: pointer; }
/* "תנאי השימוש ומדיניות הפרטיות" - a text button: inherits the sentence's type, a
   quiet underline at rest, the accent on hover. The 4px padding is cancelled by an
   equal negative margin, so the tap target grows without moving a single glyph. */
.terms-link-btn {
  display: inline;
  padding: 4px 2px; margin: -4px -2px;
  border: 0; border-radius: 5px;
  background: none;
  font: inherit; font-weight: 700;
  color: #3d5200;
  text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px;
  text-decoration-color: rgba(61, 82, 0, .35);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: color .2s var(--ease), text-decoration-color .2s var(--ease), background-color .2s var(--ease);
}
.terms-link-btn:hover { color: #2a3a00; text-decoration-color: currentColor; background-color: rgba(var(--primary-rgb), .12); }
.terms-link-btn:active { background-color: rgba(var(--primary-rgb), .22); }
.terms-link-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(var(--focus-rgb), .55); }

/* The hint is for assistive tech (Continue's aria-describedby) until a press on the
   locked Continue: then it shows, the row shakes once and the box lights up amber. */
.consent-panel__hint {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
  margin: 0;
}
.consent-panel.is-nudged:not(.is-complete) .consent-panel__hint {
  position: static; width: auto; height: auto; overflow: visible; clip-path: none; white-space: normal;
  margin: 2px 10px 0; padding-inline-start: 32px;
  font-size: .76rem; font-weight: 600; color: #9a4a06;
}
.consent-panel.is-nudged .consent-check { animation: consentNudge .5s var(--ease-sheet); }
.consent-check.is-missing .consent-check__box {
  border-color: #d97706;
  box-shadow: 0 0 0 3px rgba(217, 119, 6, .2);
}
@keyframes consentNudge {
  0%, 100% { transform: none; }
  20% { transform: translateX(-6px); }
  40% { transform: translateX(5px); }
  60% { transform: translateX(-3px); }
  80% { transform: translateX(2px); }
}

/* The popup. <body>-level (index.html), so it is never inside a screen's stacking
   context. The fade is a TRANSITION, not a filled animation, on purpose: a filled
   animation would keep the overlay a backdrop root and silently disable the blur
   (see THE BUG THIS CLOSES in §2). z 450: above the detect overlay (400) and the
   cookie banner (160), which also steps aside while this is open (§8). */
.terms-modal {
  position: fixed; inset: 0; z-index: 450;
  display: flex; align-items: center; justify-content: center;
  padding: clamp(16px, 4vw, 40px);
  opacity: 0;
  transition: opacity .26s var(--ease);
}
.terms-modal[hidden] { display: none; }
.terms-modal.is-open { opacity: 1; }
.terms-modal__backdrop {
  position: absolute; inset: 0;
  background: rgba(8, 10, 12, .38);
  -webkit-backdrop-filter: blur(24px) saturate(160%);
          backdrop-filter: blur(24px) saturate(160%);
}
.terms-modal__panel {
  position: relative; z-index: 1;
  width: min(460px, 100%); max-height: min(80vh, 640px);
  overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: 14px;
  padding: 20px 22px 22px;
  border-radius: 28px;
  text-align: start; color: var(--ink-label);
  background: linear-gradient(135deg, rgba(255, 255, 255, .88) 0%, rgba(255, 255, 255, .68) 100%);
  -webkit-backdrop-filter: blur(24px) saturate(180%);
          backdrop-filter: blur(24px) saturate(180%);
  border: 1px solid rgba(255, 255, 255, .45);
  box-shadow: var(--lg-shadow), var(--lg-edge);
  transform: translateY(12px) scale(.98);
  transition: transform .34s var(--ease-spring);
}
.terms-modal.is-open .terms-modal__panel { transform: none; }
.terms-modal__head { display: flex; align-items: center; gap: 12px; }
.terms-modal__title { margin: 0; font-size: 1.12rem; font-weight: 800; color: var(--ink); }
.terms-modal__close {
  margin-inline-start: auto; flex: none;
  display: inline-grid; place-items: center;
  width: 36px; height: 36px; padding: 0; border-radius: 50%;
  border: 1px solid var(--glass-border); background: var(--glass-strong);
  color: var(--ink); font-size: 1rem; line-height: 1; cursor: pointer;
  transition: transform .3s var(--ease), background .3s var(--ease);
}
.terms-modal__body { display: grid; gap: 14px; }
.terms-modal__section h4 {
  margin: 0 0 4px;
  font-size: .78rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
  color: #3d5200;
}
.terms-modal__section p { margin: 0; font-size: .9rem; line-height: 1.6; color: var(--ink-label); }
.terms-modal__section p strong { font-weight: 800; }
/* "הבנתי" is a baseline .btn-primary (plus the §4 glass overlay); only its top gap
   is tightened for the popup. */
.terms-modal__panel .terms-modal__ok { margin-top: 4px; }

/* phones: the same bottom sheet as Compare / detect, with a working drag handle
   (sheet-gestures.js: data-sheet="terms" → clicks #termsClose) */
@media (max-width: 768px) {
  .terms-modal { align-items: flex-end; padding: 0; }
  .terms-modal__panel {
    width: 100%;
    max-height: 88vh; max-height: 88dvh;
    border-radius: 28px 28px 0 0; border-bottom: 0;
    padding-top: 6px;
    padding-bottom: calc(22px + var(--safe-b, 0px));
    transform: translateY(100%);
  }
  .terms-modal.is-open .terms-modal__panel { transform: none; }
  .terms-modal__panel .drag-indicator { margin-bottom: -6px; }
}

/* ── 7 · mobile sheets ──────────────────────────────────────────────────────
   .drag-indicator is a 96×26 grab zone around a 40×5 bar. Motion is applied by
   sheet-gestures.js through the individual `translate` property, NOT `transform`:
   both sheet panels run fill-mode:both entrance animations on `transform`, and an
   animation's held end value outranks any inline style - a transform-based drag
   would simply not move them. `translate` composes with the animation instead. */
.drag-indicator { display: none; }
@media (max-width: 768px) {
  .drag-indicator {
    display: flex; align-items: center; justify-content: center;
    width: 96px; height: 26px; margin: 0 auto;
    flex: none;
    touch-action: none; cursor: grab;
    -webkit-tap-highlight-color: transparent;
    -webkit-user-select: none; user-select: none;
  }
  .drag-indicator::before {
    content: "";
    width: 40px; height: 5px; border-radius: var(--pill);
    background: rgba(15, 23, 42, .22);
    box-shadow: inset 0 1px 1px rgba(15, 23, 42, .12), 0 1px 0 rgba(255, 255, 255, .7);
    transition: width .3s var(--ease-sheet), background .3s var(--ease);
  }
  [data-sheet].is-dragging .drag-indicator { cursor: grabbing; }
  [data-sheet].is-dragging .drag-indicator::before { width: 54px; background: rgba(15, 23, 42, .38); }
  [data-sheet].is-dragging { -webkit-user-select: none; user-select: none; }
  [data-sheet-grab] { touch-action: none; }

  /* Screen 1: the handle hugs the card's top edge, and exists only when there is a
     host modal to dismiss (html.pear-embedded - set by the inline script in
     index.html). */
  #screen-calculator .drag-indicator { margin: -20px auto 6px; }
  html:not(.pear-embedded) #screen-calculator .drag-indicator { display: none; }

  /* Compare + detect overlays become bottom sheets. */
  .pear-compare-overlay,
  .gd-overlay { align-items: flex-end; padding: 0; }
  .pcmp__panel,
  .gd-overlay__panel {
    width: 100%;
    max-height: 92vh; max-height: 92dvh;
    border-radius: 28px 28px 0 0;
    border-bottom: 0;
    padding-top: 6px;
    padding-bottom: calc(clamp(16px, 3vw, 24px) + var(--safe-b, 0px));
    overscroll-behavior: contain;
    animation: pearSheetUp .56s var(--ease-sheet) both;
  }
  .gd-overlay.show .gd-overlay__panel { animation: pearSheetUp .56s var(--ease-sheet) both; }
  .pcmp__panel .drag-indicator,
  .gd-overlay__panel .drag-indicator { margin-bottom: -10px; }
}
@keyframes pearSheetUp {
  from { transform: translate3d(0, 100%, 0); }
  to   { transform: none; }
}

/* ── 8 · cookie / local-storage banner (#cookieBanner) ───────────────────────
   Fixed to the bottom, above the help FAB (z 150). cookie-consent.js publishes its
   live height as --cookie-banner-h so the help button can step up over it instead
   of hiding underneath.
   Z-INDEX ALONE CANNOT KEEP IT UNDER THE DIALOGS. The Compare overlay (z 220) lives
   inside #screen-fitting, and .screen's z-index:1 makes every screen a stacking
   context - so that 220 only ranks it among the screen's own children, and a
   body-level banner at 160 painted straight over the open sheet (seen in a capture,
   2026-09-18). The :has() rule below steps the banner aside while any dialog is
   open instead; it slides back the moment the dialog closes, choice still pending. */
.cookie-banner {
  position: fixed; z-index: 160;
  inset-inline: 16px; bottom: calc(16px + var(--safe-b, 0px));
  max-width: 760px; margin-inline: auto;
  padding: 14px 16px;
  border-radius: 22px;
  color: var(--ink-label);
  background: linear-gradient(135deg, rgba(255, 255, 255, .80), rgba(255, 255, 255, .56));
  -webkit-backdrop-filter: var(--lg-blur);
          backdrop-filter: var(--lg-blur);
  border: var(--lg-border);
  box-shadow: var(--lg-shadow), var(--lg-edge);
  opacity: 0;
  transform: translate3d(0, calc(100% + 32px), 0);
  transition: transform .6s var(--ease-sheet), opacity .4s var(--ease);
}
.cookie-banner[hidden] { display: none; }
.cookie-banner.is-visible { opacity: 1; transform: none; }
body:has(.pear-compare-overlay.show, .gd-overlay.show, .pear-lightbox.show, .terms-modal.is-open) .cookie-banner.is-visible {
  opacity: 0; pointer-events: none;
  transform: translate3d(0, calc(100% + 32px), 0);
}
.cookie-banner__row { display: flex; align-items: center; gap: 14px; }
.cookie-banner__icon {
  flex: none; display: grid; place-items: center;
  width: 38px; height: 38px; border-radius: 50%;
  background: rgba(var(--primary-rgb), .16);
  border: 1px solid rgba(var(--primary-rgb), .32);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .6);
}
.cookie-banner__icon svg {
  width: 20px; height: 20px;
  fill: none; stroke: #3d4825; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round;
}
.cookie-banner__text {
  flex: 1; min-width: 0; margin: 0;
  font-size: .84rem; line-height: 1.5; font-weight: 500; color: var(--ink-label);
}
.cookie-banner__actions { flex: none; display: flex; gap: 8px; }
/* The banner's buttons as first shipped (they have no pre-glass baseline of their
   own): Urbanist 800 like every baseline CTA, a ghost and a pear-tinted primary. The
   §4 overlay adds the glass sheen, frame and physics on top, as for every button. */
.cookie-btn {
  position: relative; overflow: hidden;
  min-height: 42px; padding: 10px 18px;
  border-radius: var(--pill);
  font-family: "Urbanist", "Inter", sans-serif; font-size: .88rem; font-weight: 800; letter-spacing: .01em;
  cursor: pointer; white-space: nowrap;
  transition: transform .35s var(--ease-sheet), background .3s var(--ease), box-shadow .3s var(--ease);
}
.cookie-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px #fff, 0 0 0 4px rgba(var(--focus-rgb), .55); }
.cookie-btn--ghost {
  color: var(--ink-label);
  background: rgba(255, 255, 255, .6);
  border: 1px solid rgba(15, 23, 42, .12);
}
.cookie-btn--ghost:hover { background: rgba(255, 255, 255, .88); }
.cookie-btn--ghost[aria-expanded="true"] { background: rgba(15, 23, 42, .06); }
.cookie-btn--primary {
  color: var(--pear-deep);
  background: linear-gradient(180deg, rgba(var(--primary-rgb), .38) 0%, rgba(var(--primary-rgb), .22) 100%);
  border: 1px solid rgba(var(--primary-rgb), .55);
}

.cookie-settings {
  display: grid; gap: 8px;
  margin-top: 12px; padding-top: 12px;
  border-top: 1px solid rgba(15, 23, 42, .08);
}
.cookie-settings[hidden] { display: none; }
.cookie-banner.is-settings-open .cookie-settings { animation: pearPopIn .45s var(--ease-sheet) both; }
.cookie-pref {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 10px 12px; border-radius: 14px;
  background: rgba(255, 255, 255, .5);
  border: 1px solid rgba(255, 255, 255, .65);
}
label.cookie-pref { cursor: pointer; }
.cookie-pref__copy { display: grid; gap: 2px; font-size: .8rem; line-height: 1.45; color: var(--ink-secondary); }
.cookie-pref__copy strong { font-size: .86rem; color: var(--ink-label); }
.cookie-pref__always { flex: none; font-size: .74rem; font-weight: 700; color: #3d4825; white-space: nowrap; }
.cookie-switch { position: relative; flex: none; width: 46px; height: 28px; }
.cookie-switch input {
  position: absolute; inset: 0; z-index: 2;
  width: 100%; height: 100%; margin: 0;
  opacity: 0; cursor: pointer;
}
.cookie-switch__track {
  position: absolute; inset: 0; border-radius: var(--pill);
  /* a recessed glass channel: frost over a slate wash, sunk by inner shadow */
  background: var(--lgb-sheen), linear-gradient(180deg, rgba(15, 23, 42, .16), rgba(15, 23, 42, .1));
  box-shadow: inset 0 1px 3px rgba(15, 23, 42, .22), inset 0 -1px 1px rgba(255, 255, 255, .5);
  transition: background .3s var(--ease);
}
.cookie-switch__thumb {
  position: absolute; top: 3px; inset-inline-start: 3px;
  width: 22px; height: 22px; border-radius: 50%;
  /* a glossy glass bead rather than a flat disc */
  background: radial-gradient(120% 90% at 50% 12%, #fff 0%, #f4f5f7 60%, #e6e8ec 100%);
  box-shadow: 0 3px 8px -2px rgba(15, 23, 42, .3), 0 1px 1px rgba(15, 23, 42, .12),
              inset 0 1px 1px rgba(255, 255, 255, .9);
  transition: transform .42s var(--ease-spring);
}
.cookie-switch input:checked + .cookie-switch__track {
  background: linear-gradient(180deg, rgba(255, 255, 255, .34) 0%, rgba(255, 255, 255, 0) 55%),
              linear-gradient(180deg, rgb(140, 180, 14), var(--primary-solid));
}
.cookie-switch input:checked + .cookie-switch__track .cookie-switch__thumb { transform: translateX(18px); }
html[dir="rtl"] .cookie-switch input:checked + .cookie-switch__track .cookie-switch__thumb { transform: translateX(-18px); }
.cookie-switch input:focus-visible + .cookie-switch__track { box-shadow: 0 0 0 2px #fff, 0 0 0 4px rgba(var(--focus-rgb), .55); }
.cookie-settings__save { justify-self: end; }

/* the help FAB steps up over the banner while it is showing */
body.has-cookie-banner .pear-help-widget {
  bottom: calc(var(--cookie-banner-h, 0px) + 28px + var(--safe-b, 0px));
  transition: bottom .5s var(--ease-sheet);
}

@media (max-width: 600px) {
  .cookie-banner { inset-inline: 10px; bottom: calc(10px + var(--safe-b, 0px)); padding: 14px; border-radius: 20px; }
  .cookie-banner__row { flex-wrap: wrap; gap: 12px; }
  .cookie-banner__text { flex-basis: calc(100% - 52px); }
  .cookie-banner__actions { width: 100%; }
  .cookie-banner__actions .cookie-btn { flex: 1; }
  .cookie-settings__save { justify-self: stretch; }
}

/* ── reduced motion: every new movement collapses to an instant state change ── */
@media (prefers-reduced-motion: reduce) {
  .form-group input:focus,
  .form-group select:focus,
  #otpInput:focus { transform: none; }
  #screen-calculator.active #identityForm,
  #screen-calculator.active #screen-otp,
  #screen-calculator.active #sizeForm,
  .result-actions.is-ready .consent-panel,
  .consent-panel.is-nudged,
  .cookie-banner.is-settings-open .cookie-settings,
  .pcmp__panel,
  .gd-overlay.show .gd-overlay__panel { animation: none; }
  .cookie-banner { transition: opacity .2s linear; transform: none; }
  .consent-check__box,
  .consent-check__box svg,
  .cookie-switch__thumb { transition: none; }
  /* §4: no hover growth and no sheen fade. The press scale stays - it is feedback
     that the tap registered, not decorative motion. Same weights as §4's, later. */
  :is(.btn-primary, .btn-ghost, .btn-capture, .btn-add-cart, .btn-watch, .btn-download,
      .btn-update-measure, .btn-edit-measurements, .cart-btn, .cookie-btn, .pear-compare-bar,
      .plb-btn, .pear-sz-btn, .pear-rz-btn, .pcmp__close, .terms-modal__close, .gd-close, .pear-lightbox__close,
      .pear-help-widget__btn, .pear-help-widget__close, .camera-card__flip,
      .pip-retake, .pip-clear, .profile-btn, .profile-dropdown__logout,
      .cart-line__remove):hover:not(:disabled):not([aria-disabled="true"]):not(.is-done):not(.is-loading),
  #btn-next-screen:hover:not(:disabled):not([aria-disabled="true"]) { transform: none; }
  .cl-arrow:hover { transform: translateY(-50%); }
  body :is(.btn-primary, .btn-ghost, .btn-capture, .btn-add-cart, .btn-watch, .btn-download,
           .btn-update-measure, .btn-edit-measurements, .cart-btn, .cookie-btn, .pear-compare-bar,
           .plb-btn, .pear-sz-btn, .pear-rz-btn, .pcmp__close, .terms-modal__close, .gd-close, .pear-lightbox__close,
           .pear-help-widget__btn, .pear-help-widget__close, .camera-card__flip, .cl-arrow,
           .pip-retake, .pip-clear, .profile-btn, .profile-dropdown__logout,
           .cart-line__remove)::before { transition: none; }
  .consent-check input:checked + .consent-check__box { animation: none; }
  .consent-panel.is-nudged .consent-check { animation: none; }
  .terms-modal, .terms-modal__panel { transition: none; }
  .step-progress-fill { transition: none; }
}
