/* ============================================================================
   ECOLAB · VIRTUAL ENZYME EXPERIENCE — STAGE
   ----------------------------------------------------------------------------
   Everything the experience needs, and nothing the host page provides.
   This file must stay self-contained: the stage ships inside an iframe with no
   Ecolab chrome around it, so it may not depend on chrome.css or on any style
   the review page happens to supply.

   Rules enforced here:
     - every selector is scoped under #enzyme-stage, so nothing leaks to a host
     - the reset is scoped too; there is no bare `*` or `body` rule
     - geometry uses container query units, never vw/vh. Inside the iframe the
       viewport IS the frame, but during review the stage sits mid-page, and
       viewport units give different answers in those two cases.
     - nothing is position:fixed

   GEOMETRY MODEL
   The Figma frame is 1376 x 774 (exact 16:9). --u resolves to one design pixel,
   so every measurement below is the literal number from Figma:
       width: calc(1296 * var(--u))   <-  1296px in the design
   At a 1376px-wide container --u == 1px and the build is pixel-exact.
   ========================================================================== */

#enzyme-stage {
  /* --- container --------------------------------------------------------
     inline-size, not size. Only cqw is used anywhere, and inline-size is what
     allows the stage to take an auto height in mobile mode — `size` requires a
     definite height in both axes and would collapse the moment the fixed
     aspect ratio comes off. */
  container-type: inline-size;
  container-name: stage;
  position: relative;
  width: 100%;

  /* WIDTH CAP. 1376 is Ecolab's own content column — their `.page` is
     max-width:1440 with 32px gutters — so capping here costs nothing in the
     real embed and is what lets browser zoom work. See --u below. */
  max-width: calc(1376 * var(--u));
  margin-inline: auto;

  /* ONE SHARED HEIGHT. Nothing is pinned to the stage any more: the tracker,
     all nine screens and the nav share a single grid cell, so the cell — and
     therefore the stage — is always as tall as its tallest occupant. The box
     cannot jump between slides, and if a reader's text-spacing settings grow
     one screen, every screen grows with it. That is also what makes the iframe
     contract honest: one height, one aspect ratio, correct on all nine. */
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  min-height: calc(774 * var(--u));

  background: var(--surface-page);

  /* ONE DESIGN PIXEL — and it is a REAL pixel, at every width.
     This used to be calc(100cqw / 1376), which scaled the whole drawing with
     its container. That is why 1.4.4 Resize Text failed outright: zoom shrinks
     the CSS viewport by exactly the factor it magnifies by, so a container-
     derived type size cancels the zoom and 200% delivered x1.000 — measured,
     at every window width tested, and x0.738 in one case. No width-responsive
     type unit can survive that. Fixed px can: 200% zoom simply doubles it.
     The cost is that the composition no longer scales to fit — it has to
     REFLOW below 1376 instead, which is what taking the pins out buys us. */
  --u: 1px;

  /* --- brand ------------------------------------------------------------ */
  --brand-blue: #006bd3;
  --brand-navy: #093675;
  --rule-on-navy: #80b5e9;

  /* --- ink -------------------------------------------------------------- */
  /* Figma carries three near-identical body greys (#32373E #313233 #3A3A3A).
     They are not visually distinguishable. Kept separate so the build matches
     the file exactly; collapse to --ink-body if the design ever consolidates. */
  --ink-body: #32373e;
  --ink-strong: #313233;
  --ink-muted: #3a3a3a;
  --ink-inverse: #ffffff;

  /* --- surface ---------------------------------------------------------- */
  --surface-page: #ffffff;
  --surface-caustic-strip: #eaeaea;
  /* AA 1.4.11 — this token was doing two jobs and could not be corrected as
     one. As a BOUNDARY (card, tile and ring borders) #D9D9D9 measured 1.41:1
     and had to darken. As a FILL (the caustic tab, the caustic ledger panel,
     the video well) it is a surface and needs no ratio of its own — darkening
     it dropped the tab label to 2.47:1 and put the whole ledger on #949494.
     So the fill keeps the Figma grey and boundaries get their own token. */
  --surface-inactive: #d9d9d9;
  --boundary: #949494; /* 3.03:1 on white */
  --surface-blue-tint: rgba(0, 107, 211, 0.1);
  /* AA 1.4.11: was #B6B6B6 at 2.03:1. */
  --rule-on-white: #949494;
  --progress-track: rgba(0, 107, 211, 0.2);
  /* --progress-edge is gone with the tracker edge it drew — see the .track li
     rule for why that edge was not required. --tint-enz carried the same value
     for a different job and stays: it is the m5 chart tint, which DID fail
     1.4.11 at 1.93:1 as #8FBFF0 and needs its 3.09:1. */
  --tint-enz: #5b96d8;

  /* --- caustic / hazard -------------------------------------------------- */
  /* Figma uses four reds where it appears to intend one or two. Preserved
     individually rather than merged, because merging them is a design call. */
  --caustic-red: #c4262e;
  /* AA 1.4.3: was #D32F2F at 3.53:1 on the #D9D9D9 tab. Darkened to 5.31:1. */
  --caustic-red-tab: #a32126;
  --danger-word: #ec3a40;
  --danger-border: #ef3a43;

  /* WCAG AA — the values flagged in the RND 01 audit are CORRECTED above,
     each with its measured before and after. RND 01 keeps the originals. */

  /* --- type ------------------------------------------------------------- */
  --font-sans: "Roboto", system-ui, sans-serif;
  --font-mono: "Roboto Mono", ui-monospace, monospace;
  /* Figma sets the tab labels in Public Sans. It is the only element using it
     and the Ecolab font package does not include it, so it falls back to mono.
     Substitution, not a match — see _spec/tokens.md. */
  --font-tab: var(--font-mono);

  /* --- motion ----------------------------------------------------------- */
  /* Three curves only, per the Ecolab house vocabulary. */
  --ease-fade: ease;
  --ease-size: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-travel: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-overshoot: cubic-bezier(0.34, 1.56, 0.64, 1);
  --dur-state: 0.2s;
  --dur-enter: 0.7s;
  --dur-build: 1s;
  --stagger: 0.13s;

  font-family: var(--font-sans);
  color: var(--ink-body);
  -webkit-font-smoothing: antialiased;
  text-size-adjust: 100%;

  /* Last-resort guard against a word leaving the frame, inherited by
     everything. Measured case: at a 320px viewport with the WCAG 1.4.12
     spacing sheet applied, "Different" in the 66px cover headline needs more
     than the 280px of content width available and ran 32px past the edge.
     Shrinking the headline would fix it and break 1.4.4, so the word breaks
     instead. It only ever engages when a single word genuinely cannot fit,
     which is exactly the case where the alternative is losing it.

     `anywhere`, not `break-word`. They break identically on screen, but only
     `anywhere` also reduces the box's MIN-CONTENT width — and that is what
     matters here, because these are flex items whose automatic minimum size is
     the longest unbreakable word. With `break-word` the headline still measured
     332px inside a 280px column and escaped by the same 32px. */
  overflow-wrap: anywhere;
}

/* Scoped reset. Deliberately not a bare `*` — this must not reach a host page. */
#enzyme-stage *,
#enzyme-stage *::before,
#enzyme-stage *::after {
  box-sizing: border-box;
}
#enzyme-stage p,
#enzyme-stage h1,
#enzyme-stage h2,
#enzyme-stage h3,
#enzyme-stage figure,
#enzyme-stage ol,
#enzyme-stage ul,
#enzyme-stage dl,
#enzyme-stage dd,
#enzyme-stage blockquote {
  margin: 0;
}
/* lists are used structurally here, never as bullets. The browser default 1em
   margin and 40px padding-left silently shifted the progress tracker 16px down
   and squeezed its segments. */
#enzyme-stage ol,
#enzyme-stage ul {
  padding: 0;
  list-style: none;
}
#enzyme-stage img {
  display: block;
  max-width: 100%;
}
#enzyme-stage button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
}
#enzyme-stage :focus-visible {
  outline: calc(3 * var(--u)) solid var(--brand-navy);
  outline-offset: calc(2 * var(--u));
}
/* ...except the screen container itself. stage.js gives each screen
   tabindex="-1" and focuses it on every navigation so a screen reader announces
   the new screen. It is a programmatic focus target, never a keyboard tab stop,
   so 2.4.7 Focus Visible does not ask for an indicator on it — and the rule
   above was drawing a 3px navy ring around the entire stage.

   This is not new: the ring was always painted, and the stage's overflow:hidden
   clipped it. Taking that clip off for 1.4.12 revealed it, as a dark border on
   load that vanished the moment the pointer moved (because :focus-visible keys
   off the last input type, and a reload is a keyboard action). Focus still
   moves and is still announced; only the ring is suppressed. */
#enzyme-stage .screen:focus,
#enzyme-stage .screen:focus-visible {
  outline: none;
}

/* ============================================================================
   SCREENS
   All nine live in the DOM at once. Only the active one is visible or
   focusable; the rest are inert so keyboard and AT never reach them.
   ========================================================================== */

#enzyme-stage .screen {
  /* All nine in one cell. They stay in the grid whether or not they are the
     active screen — visibility:hidden, not display:none — because a screen
     that is out of flow contributes no height, and the shared-height promise
     depends on every screen being measured. visibility:hidden already removes
     them from the tab order and the accessibility tree; `inert` below closes
     the gap for anything that ignores that. */
  grid-area: 1 / 1;
  min-width: 0;

  /* The tracker occupies the top 44 (y38 + 6) and the nav the bottom 68
     (48 tall + 20 clear). Both overlay this cell, so every screen reserves
     that space rather than being positioned around it. */
  padding: calc(96 * var(--u)) calc(40 * var(--u)) calc(88 * var(--u));

  display: flex;
  flex-direction: column;

  opacity: 0;
  visibility: hidden;
  transition:
    opacity 0.34s var(--ease-fade),
    visibility 0s linear 0.34s;
}
#enzyme-stage .screen:not(.on) {
  pointer-events: none;
}
#enzyme-stage .screen.on {
  opacity: 1;
  visibility: visible;
  transition:
    opacity 0.34s var(--ease-fade),
    visibility 0s;
}

/* ============================================================================
   PROGRESS TRACKER
   Figma: y=38, height 6, eight 160px segments with seven 2px dividers.
   That totals 1294 against a 1296 content width, so the design is two pixels
   short of its own margin. Built with flex so it resolves to exactly 1296
   rather than reproducing the rounding error.
   ========================================================================== */

#enzyme-stage .track {
  /* Shares the screens' cell and pins itself to the top of it with margin
     rather than coordinates, so it rides the stage instead of being nailed to
     a y value. Width comes from the cell, not from a 1296 literal — that is
     what lets it reflow below 1376. */
  grid-area: 1 / 1;
  align-self: start;
  margin: calc(38 * var(--u)) calc(40 * var(--u)) 0;
  height: calc(6 * var(--u));
  display: flex;
  gap: calc(2 * var(--u));
  z-index: 5;
}
/* Each segment is an empty track; the <i> inside it is the fill. */
/* NO EDGE ON THE UNFILLED SEGMENT, and that is deliberate — do not "fix" it.

   The first remediation round put a 1px #5b96d8 edge here to lift the empty
   segment off white, on the reading that 1.4.11 wanted its boundary
   perceivable. Removed on Sean's decision, 2026-08-03, because that reading
   does not hold and the edge was visibly wrong: it made the empty segments read
   as outlined boxes instead of a faint track, which is not what Figma draws.

   1.4.11 covers two things: user interface COMPONENTS and their states, and
   GRAPHICS REQUIRED TO UNDERSTAND THE CONTENT. This tracker is neither. It
   carries no controls and is not interactive, so it is not a component; and it
   is marked aria-hidden="true", which is the build itself declaring it is not
   required to understand anything. The old fix wanted it decorative for screen
   readers and essential for contrast at the same time, which cannot both be
   true. Progress is independently carried by Next/Back, by the index screen
   listing all six modules, and by each screen naming itself.

   If the tracker is ever given a real accessible presence — dropping
   aria-hidden and adding a "Step 4 of 8" for AT — then it becomes information,
   and the 3:1 edge has to come back with it. The two go together. */
#enzyme-stage .track li {
  flex: 1;
  background: var(--progress-track);
  overflow: hidden;
}
#enzyme-stage .track li i {
  display: block;
  height: 100%;
  background: var(--brand-navy);
  transform: scaleX(0);
  transform-origin: left;
}
/* Segments already behind you are history — they snap, they do not replay. */
#enzyme-stage .track li.done i {
  transform: scaleX(1);
}
/* MOVE 4, GEOMETRY GROWTH. The segment for the screen you just landed on wipes
   left to right on the Material curve, so the tracker fills as the page builds
   rather than blinking to a new colour. Slight delay so it reads as part of the
   build-in and not as a separate event. */
#enzyme-stage .track li.now i {
  background: var(--brand-blue);
  transform: scaleX(1);
  transition: transform 0.9s var(--ease-size) 0.15s;
}

/* ============================================================================
   NAV BUTTONS
   Figma pins them to the bottom, Next's right edge landing on the 40px margin.
   ========================================================================== */

#enzyme-stage .nav {
  /* Same cell, held to the bottom by alignment rather than by coordinates.
     Figma lands Next's right edge on the 40px margin, and it still does.

     Full width now (justify-self: stretch, was end) so the jump group can sit
     bottom-left while Back and Next keep the bottom-right corner they have
     always had. space-between is what holds the two apart at every width. */
  grid-area: 1 / 1;
  align-self: end;
  justify-self: stretch;
  margin: 0 calc(40 * var(--u)) calc(20 * var(--u));
  display: flex;
  justify-content: space-between;
  gap: calc(12 * var(--u));
  z-index: 4;
  /* THE BAR IS NOW AS WIDE AS THE STAGE, so without this it becomes an
     invisible sheet across the bottom of all nine screens, swallowing clicks
     on anything under it — the m6 steppers and the m5 transport sit right at
     that edge. The bar catches nothing; only the buttons do. */
  pointer-events: none;
}
#enzyme-stage .nav-jump,
#enzyme-stage .nav-step {
  display: flex;
  gap: calc(12 * var(--u));
}
#enzyme-stage .nav .btn {
  pointer-events: auto;
}
#enzyme-stage .btn {
  display: inline-flex;
  align-items: center;
  gap: calc(10 * var(--u));
  height: calc(48 * var(--u));
  padding: 0 calc(30 * var(--u));
  border-radius: calc(999 * var(--u));
  background: var(--brand-blue);
  color: var(--ink-inverse);
  font: 500 calc(20 * var(--u)) / 1 var(--font-sans);
  white-space: nowrap;
  transition:
    transform 0.22s var(--ease-fade),
    background var(--dur-state) var(--ease-fade),
    opacity var(--dur-state) var(--ease-fade);
}
#enzyme-stage .btn:hover:not(:disabled) {
  transform: translateY(calc(-2 * var(--u)));
  background: #0a7bef;
}
#enzyme-stage .btn:disabled {
  opacity: 0.3;
  cursor: default;
}
/* display:inline-flex above outranks the UA's [hidden]{display:none}, so a
   hidden button would still paint. The cover has no Back button in Figma. */
#enzyme-stage .btn[hidden] {
  display: none;
}
#enzyme-stage .btn.ghost {
  background: transparent;
  color: var(--brand-navy);
  box-shadow: inset 0 0 0 calc(2 * var(--u)) var(--brand-navy);
}
#enzyme-stage .btn.ghost:hover:not(:disabled) {
  background: rgba(9, 54, 117, 0.06);
}
#enzyme-stage .btn .chev {
  width: calc(5 * var(--u));
  height: calc(12 * var(--u));
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}

/* ============================================================================
   MOTION — the house vocabulary, retimed for a click-through
   The reference builds trigger on scroll via IntersectionObserver. Nothing here
   scrolls, so the trigger is screen arrival instead. The timings, curves and
   stagger rate are unchanged from the CDP and Insights Hub builds.
   ========================================================================== */

/* MOVE 1 — RISE AND FADE. Default entrance: 26px up, 700ms, staggered 0.13s. */
#enzyme-stage .rev {
  opacity: 0;
  transform: translateY(calc(26 * var(--u)));
  transition:
    opacity var(--dur-enter) var(--ease-fade),
    transform var(--dur-enter) var(--ease-fade);
  transition-delay: var(--d, 0s);
}
#enzyme-stage .screen.on .rev.in {
  opacity: 1;
  transform: none;
}

/* MOVE 2 — LINE-MASK RISE. Display headlines only. Each line rises from 112%
   below behind a mask, on the long expo-out curve. The slowest move here. */
#enzyme-stage .lines .ln {
  display: block;
  overflow: hidden;
}
#enzyme-stage .lines .ln > span {
  display: block;
  transform: translateY(112%);
  transition: transform 1s var(--ease-travel);
  transition-delay: var(--d, 0s);
}
#enzyme-stage .screen.on .lines.in .ln > span {
  transform: none;
}

/* MOVE 4 — GEOMETRY GROWTH. Bars and tracks grow from zero on the size curve;
   any label inside is held transparent until the growth has stopped. */
#enzyme-stage .grow {
  transition: transform var(--dur-build) var(--ease-size) var(--d, 0s);
  transform: scaleX(0);
  transform-origin: left;
}
#enzyme-stage .screen.on .grow.in {
  transform: scaleX(1);
}

/* LEFT-TO-RIGHT BUILD. Text wipes in from its own left edge rather than
   fading in place, so a row of data reads as being written across.
   Unlike .rev, the runtime does NOT auto-assign --d to these: the screens that
   use them are orchestrating a specific order, so they set their own delays. */
/* The transition lives on .in, NOT on the base state. Unlike .rev these
   classes are added at runtime, so a transition on the hidden state would
   animate the HIDING too — opacity would spend its whole delay still visible
   and then fade out just as it was being asked to fade in. Declaring it here
   means adding .wipe snaps to hidden and only the reveal is animated. */
#enzyme-stage .wipe {
  clip-path: inset(0 100% 0 0);
  opacity: 0;
}
#enzyme-stage .screen.on .wipe.in {
  clip-path: inset(0 0 0 0);
  opacity: 1;
  transition:
    clip-path 0.55s var(--ease-size) var(--d, 0s),
    opacity 0.3s var(--ease-fade) var(--d, 0s);
}

/* RIPPLE DOWN. Blocks arrive from just above and settle, staggered top to
   bottom, on the travel curve. Used for elements that should land after the
   text they belong to has finished building. */
#enzyme-stage .drop {
  opacity: 0;
  transform: translateY(calc(-10 * var(--u)));
}
#enzyme-stage .screen.on .drop.in {
  opacity: 1;
  transform: none;
  transition:
    opacity 0.32s var(--ease-fade) var(--d, 0s),
    transform 0.44s var(--ease-travel) var(--d, 0s);
}

/* Reduced motion: land the finished state. Never hide, never freeze mid-build. */
@media (prefers-reduced-motion: reduce) {
  #enzyme-stage .rev,
  #enzyme-stage .lines .ln > span,
  #enzyme-stage .grow,
  #enzyme-stage .wipe,
  #enzyme-stage .drop {
    transition: none;
  }
  #enzyme-stage .rev {
    opacity: 1;
    transform: none;
  }
  #enzyme-stage .wipe {
    clip-path: none;
    opacity: 1;
  }
  #enzyme-stage .drop {
    opacity: 1;
    transform: none;
  }
  #enzyme-stage .lines .ln > span {
    transform: none;
  }
  #enzyme-stage .grow {
    transform: scaleX(1);
  }
  #enzyme-stage .screen {
    transition: none;
  }
  /* the tracker still shows position, it just arrives filled instead of wiping */
  #enzyme-stage .track li.now i {
    transition: none;
  }
}

/* ============================================================================
   SHARED TYPE ROLES
   Sizes are the literal Figma values. Roles that recur across screens live
   here; anything used on exactly one screen lives in that screen's own block.
   ========================================================================== */

#enzyme-stage .eyebrow {
  font: 700 calc(12 * var(--u)) / 1.2 var(--font-mono);
  letter-spacing: calc(2.2 * var(--u));
  text-transform: uppercase;
  color: var(--brand-navy);
}
#enzyme-stage .h-cover {
  font: 700 calc(66 * var(--u)) / calc(66 * var(--u)) var(--font-sans);
  color: var(--brand-blue);
}
#enzyme-stage .h-screen {
  font: 700 calc(40 * var(--u)) / calc(41 * var(--u)) var(--font-sans);
  color: var(--brand-blue);
}
#enzyme-stage .dek {
  font: 300 calc(22 * var(--u)) / calc(28 * var(--u)) var(--font-sans);
  color: var(--ink-body);
}
#enzyme-stage .dek-sm {
  font: 300 calc(21 * var(--u)) / calc(28 * var(--u)) var(--font-sans);
  color: var(--ink-body);
}
#enzyme-stage .source {
  font: 400 calc(11 * var(--u)) / 1.4 var(--font-sans);
  letter-spacing: calc(0.6 * var(--u));
  color: var(--ink-body);
}
#enzyme-stage .tag {
  display: inline-flex;
  align-items: center;
  height: calc(18 * var(--u));
  padding: 0 calc(7 * var(--u));
  border-radius: calc(2 * var(--u));
  font: 500 calc(9.5 * var(--u)) / 1 var(--font-mono);
  letter-spacing: calc(0.5 * var(--u));
  text-transform: uppercase;
  color: var(--ink-inverse);
}
#enzyme-stage .tag.red {
  background: var(--caustic-red);
}
#enzyme-stage .tag.blue {
  background: var(--brand-blue);
}
#enzyme-stage .tag.navy {
  background: var(--brand-navy);
}

/* Visually hidden but available to assistive tech. */
#enzyme-stage .sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ============================================================================
   MOBILE MODE
   ----------------------------------------------------------------------------
   Below 1024px of stage width the 16:9 lock comes off and the deck becomes a
   portrait, auto-height, genuinely stacked layout.

   WHY A BREAKPOINT IS REQUIRED, not a preference. Above this line the whole
   stage is one proportionally scaled drawing: --u is a fraction of the
   container, so everything shrinks together and the composition stays exact.
   Measured, that degrades fast — at 1024 the smallest labels render at 7.1px,
   at 768 they are 5.3px, and at a 390px phone the entire experience is 219px
   tall with 2.7px type. Two AA requirements fail as a direct result:

     1.4.10 Reflow      content must work at a 320px equivalent. A 16:9 frame
                        that only ever scales cannot reach that at any size.
     1.4.4 Resize Text  because type is sized in container units, browser zoom
                        scales the container too, so zooming does nothing for
                        the reader. That is a property of the approach, not a
                        bug — only real type sizes fix it.

   So below the breakpoint --u becomes a REAL pixel. Every `calc(N * var(--u))`
   in the deck then resolves to N literal px, which is why each screen needs its
   own mobile block: the desktop geometry is drawn against a 1376px canvas and
   would overflow a phone many times over. The shared pieces are handled here;
   the per-screen layouts live in each screen file.

   The mode is driven by a class the runtime sets from a ResizeObserver, NOT by
   a container query. A container query can only style a container's
   DESCENDANTS, and the stage's own aspect-ratio and height have to change.

   HEIGHT. The stage grows to its active screen and reports that height to the
   parent by postMessage, so the iframe can follow. See stage.js and the
   embed-parent snippet in README. Only the active screen is in flow; the rest
   are display:none, so the stage measures exactly one screen.
   ========================================================================== */

#enzyme-stage.is-mobile {
  /* Out of the shared-height grid and into ordinary block flow: tracker,
     then the active screen, then the nav.

     Mobile deliberately does NOT hold one shared height. Matching every slide
     to the tallest would hand the cover about 1,500px of dead scroll to match
     the bill, which stacks two receipts. A phone reader is already scrolling,
     so a per-screen height reads as normal responsive behaviour — and the
     parent follows it by postMessage either way, so the iframe contract is
     unchanged. The shared height is a promise about the DESKTOP stage, where
     it buys Ecolab one aspect-ratio value that is correct on all nine. */
  display: block;
  min-height: 0;
  max-width: none;
  --m: 20px; /* mobile page margin */
  padding-bottom: calc(20 * var(--u));
}

/* Only the live screen occupies space, so the stage height is that screen's. */
#enzyme-stage.is-mobile .screen {
  grid-area: auto;
  position: relative;
  inset: auto;
  padding: 0 var(--m);
  opacity: 1;
  visibility: visible;
  transition: none;
}
#enzyme-stage.is-mobile .screen:not(.on) {
  display: none;
}

/* --- tracker ------------------------------------------------------------- */
#enzyme-stage.is-mobile .track {
  position: relative;
  left: auto;
  top: auto;
  width: auto;
  height: calc(4 * var(--u));
  margin: var(--m) var(--m) 0;
  gap: calc(2 * var(--u));
}

/* --- nav ------------------------------------------------------------------
   In flow beneath the screen rather than pinned, so it can never sit on top of
   content whose height it cannot know. */
#enzyme-stage.is-mobile .nav {
  position: relative;
  right: auto;
  bottom: auto;
  margin: calc(24 * var(--u)) var(--m) 0;
  gap: calc(10 * var(--u));
  /* A GRID, NOT A WRAPPING FLEX ROW (Sean, RND 10). Dropping Contact us got it
     to three buttons, but flex-wrap only moved the problem: three buttons at
     375 measured 335 wide inside a 335 bar, zero slack, and at 320 Next fell
     to a row of its own again. A layout that fits by exactly 0px is not fixed,
     it is about to break on the next font substitution.

     grid-auto-flow: column with grid-auto-columns: 1fr cannot wrap, because
     there is no second line to wrap to. The columns are also generated per
     VISIBLE child, which is the property that matters here: the cover hides
     Index and Back, and one child means one column, so Next still stretches
     the full width there exactly as it did before. Three children means
     thirds. Nothing special-cases the cover.

     No space-between: the left/right split is a desktop idea, and holding it
     here would strand a lone button against each edge. */
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
}

/* CONTACT US IS DESKTOP-ONLY NOW (Sean, RND 10). Four buttons could not hold
   one row on a phone, and the way they broke was the problem rather than the
   wrapping itself: Index, Contact us and Back took row one and NEXT — the
   primary action, the only solid button in the deck — dropped to a row of its
   own underneath. The most important control ended up looking like an
   afterthought.

   Three buttons fit one row, and Next keeps its weight.

   Nothing is lost by removing it. Contact us is a shortcut to the close
   screen, and that screen is still reachable two other ways: Index lists it,
   and Next walks to it. This hides a convenience, never a destination, which
   is why it does not read as a loss of functionality under zoom.

   Width-driven on purpose, unlike the cover headline's pointer query. That one
   is about type size, where crossing the breakpoint by zooming would cancel
   the zoom. This is about whether four buttons fit a row, which is a question
   about width and nothing else, so a zoomed desktop should answer it the same
   way a phone does. */
#enzyme-stage.is-mobile #ezContact {
  display: none;
}

/* INDEX ALSO DROPS OFF THE CLOSE SCREEN ON A PHONE (Sean, RND 10). Next reads
   "Start over" on the last screen, and start over already is the way back to
   the beginning, so Index sitting beside it offers the same thing twice. Two
   buttons that mean roughly the same is worse than one that means it clearly.

   Close only. Index earns its place on m1 through m6, where Start over does
   not exist and the grid is genuinely somewhere else to go.

   [data-screen] is set by render() in stage.js. The nav is a sibling of the
   screens, not a child, so there is no descendant selector that could reach
   this without it.

   Width-driven, like the Contact us rule above and unlike the type rules: this
   is about how many buttons belong in a row, which is a question about width,
   so a zoomed desktop should answer it the same way a phone does. */
#enzyme-stage.is-mobile[data-screen="close"] #ezIndex {
  display: none;
}
/* The two groups dissolve on mobile so all four buttons wrap as one set
   against the nav itself. Grouping them would wrap group-by-group and leave
   ragged half-rows. */
#enzyme-stage.is-mobile .nav-jump,
#enzyme-stage.is-mobile .nav-step {
  display: contents;
}
#enzyme-stage.is-mobile .btn {
  flex: 1 1 auto;
  justify-content: center;
  height: calc(48 * var(--u));
  /* 18 -> 10 (RND 10). The grid above decides the width now, so this padding
     is no longer doing layout work — it is only the distance from the pill
     edge to the label, and at a 320 stage each column is 86.7px, of which 36
     went to padding and left 50.7 for a label that measures 55. The label was
     about to overflow its own button.

     Padding, deliberately, and NOT font-size. Shrinking the label is the 1.4.4
     trap this file warns about three times: is-mobile keys off stage width, a
     desktop reader at 200% zoom lands in it, and a smaller label there cancels
     the zoom they asked for. The box may change on a phone. The type may not. */
  padding: 0 calc(10 * var(--u));
  min-width: 0;
  /* No font-size here. It was 16 against the desktop 20, and that single
     reduction was enough to hold the button at x1.600 under 200% zoom while
     every other role had reached x2.000 — same mechanism as the block below.
     The button keeps its own box on mobile (full-width, tighter padding); the
     label is the desktop size like everything else. */
}

/* --- one type scale, both modes -------------------------------------------
   These used to shrink the type for mobile: h-cover 66->34, h-screen 40->26,
   dek 22->16, eyebrow 12->11. That is what made WCAG 1.4.4 unfixable.

   Zoom shrinks the CSS viewport by exactly the factor it magnifies by, so a
   reader at a 1376 window who zooms to 200% lands at 688 CSS px — across the
   1024 breakpoint, into the mobile scale. Measured: the headline went 66 device
   px to 68. The reader asked for double and got three percent. Every window
   from 1024 to 2048 behaved the same way, because the type got smaller by
   almost as much as the zoom made it bigger.

   Only one type scale survives that, so the reductions are gone and mobile
   inherits the desktop sizes. What changes on a phone is the LAYOUT, not the
   type — which is how conformant responsive sites have always done it.
   Measured after: x2.000 on every role at every window width tested.

   Only the tag keeps a mobile rule, and only for its box: the type inside it
   is the desktop size like everything else. */
#enzyme-stage.is-mobile .tag {
  height: calc(22 * var(--u));
  padding: 0 calc(8 * var(--u));
}

/* LEADING, not size. The display headlines are drawn solid — .h-cover is 66/66
   and .h-screen 40/41 — because on desktop each line sits in its own .ln mask
   and holds exactly one line, so there is never a line below to clear. In a
   280px column those lines wrap INSIDE a single mask, and solid leading puts
   the second line straight through the first: "Different" and "clean." were
   overlapping on the cover at 390.

   Only line-height changes here. Font-size is untouched, so this costs nothing
   against 1.4.4 — that criterion is about the size of the text, and every role
   still measures x2.000 at 200% zoom. */
#enzyme-stage.is-mobile .h-cover,
#enzyme-stage.is-mobile .h-screen {
  /* 1.2, measured, not chosen. Roboto's own line box is ~1.167em, so at 66px
     each line's glyph box is 77.0px tall. 1.1 gives a 72.6px line box and the
     lines overlapped by exactly 4.4px — visible on the cover, where
     "Different" printed into "Same line." 1.2 gives 79.2px and clears it. */
  line-height: 1.2;
}

/* Touch targets. 2.5.8 asks for 24px; anything the finger drives gets 44. */
#enzyme-stage.is-mobile button,
#enzyme-stage.is-mobile [role="tab"],
#enzyme-stage.is-mobile [role="slider"] {
  min-height: calc(24 * var(--u));
}

/* ============================================================================
   AA 1.4.11 — COMPARATOR KNOB RING
   The drag handle is the visual information that identifies the comparator, so
   its boundary has to clear 3:1. Measured on RND 01 it did not: over the pale
   plant photography on m4 the navy disc ran a 2.718:1 median with the worst
   perimeter sample at 1.275:1.

   A white ring does NOT fix it — that was tested and still left 41 of 172
   perimeter samples under 3:1, bottoming out at 1.959:1 against the light blue
   tank pixels. A two-tone ring does, because whichever backdrop the knob is
   over, one of the two rings carries: navy against light content, white
   against dark.
   ========================================================================== */
#enzyme-stage .cv-knob,
#enzyme-stage .ev-knob {
  box-shadow:
    0 0 0 calc(2 * var(--u)) #ffffff,
    0 0 0 calc(4 * var(--u)) var(--brand-navy);
}
