/* work-gemstones: page and component styles. Moved out of the page on 2026-10-10 so the
   component catalog (docs/catalog.html) can use them too. Tokens come from css/tokens.css
   and the base + text roles from css/ds.css, both linked before this file (D25). */
/* <picture> only swaps sources (see the phone placeholders); it must not change layout */
picture { display: contents; }
html {
  /* documentElement, not body, is the page's actual scrolling box — the
     100vw full-bleed sections' scrollbar-width overflow has to be clipped
     here for it to actually stop the horizontal scroll glitch */
  overflow-x: hidden;
}
/* Desktop only (Ioli, 2026-10-27: phones and tablets scroll by the browser's default, the snap system is to be reworked).
   proximity (not mandatory): pulls into place only when already close to a focus-area boundary, otherwise scrolls completely
   freely — mandatory would force a snap on every scroll gesture regardless of intent */
@media (min-width: 1280px) {
  html { scroll-behavior: smooth; scroll-snap-type: y proximity; }
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; scroll-snap-type: none; }
}

body {
  /* the full-bleed accent sections use 100vw, which includes the vertical
     scrollbar's own width and so overflows the visible viewport by a few
     px on the right — clipping it here is the standard fix, rather than
     rebuilding full-bleed without 100vw */
  overflow-x: hidden;
}

.heading-m.heading--on-accent { color: var(--text-body); }
[data-scroll]:focus { outline: none; }
/* Heading + text pair: the 12px gap used between every heading and its text */
.wg-heading-group { display: flex; flex-direction: column; gap: var(--gap-stack); width: 100%; }
/* Text colour on dark or accent surfaces (were inline style attributes) */
.wg-text--white { color: var(--text-body); }
.wg-text--neutral { color: var(--text-secondary); }
/* Semantic wrappers (lists, figures, quotes) that must not change layout. */
.wg-list-reset { list-style: none; margin: 0; padding: 0; }
/* Accent text is #7e80ff on surface-dark (--text-accent); light surfaces switch to the original accent (D19) */
.wg-callout, .wg-hint, .wg-outcomes-card { --text-accent: var(--text-accent-on-light); }
figure.wg-compare-detail__col, blockquote.statement { margin: 0; }
.wg-round > .caption-heading { color: var(--text-secondary); }

/* ---------- placeholder visuals ---------- */
.wg-visual { background: var(--surface-light); border-radius: var(--radius-card); flex-shrink: 0; }

/* ---------- area 5: rough.js sketch solutions ---------- */
.wg-sketch { position: relative; background: var(--surface-page); overflow: hidden; display: flex; align-items: center; justify-content: center; }
.wg-sketch__svg { display: block; width: 100%; height: 100%; }
/* Square wrapper matching the SVG's 270x270 viewBox 1:1 so the % positions
   below land on the same coordinate space as the art, not the (wider)
   card box — without it, preserveAspectRatio letterboxing shifts the art
   inward while these percentages stay keyed to the full box, misaligning
   text against the checkbox rows it's meant to sit beside. */
.wg-sketch__canvas { position: relative; height: 100%; aspect-ratio: 1 / 1; }
.wg-sketch__canvas .wg-sketch__svg { position: absolute; inset: 0; }
.wg-sketch__labels { position: absolute; inset: 0; pointer-events: none; }
/* @raw-ok:start artwork: hand-lettered sketch labels placed on the 270x270 sketch art (Figma) */
.wg-sketch__label {
  position: absolute; transform: translateY(-50%); font-family: var(--sketch-font);
  color: var(--text-body); line-height: 1; white-space: nowrap;
}
.wg-sketch__label--sapphire     { top: 25%;    left: 27%; font-size: var(--sketch-title-size); letter-spacing: 0.02em; }
.wg-sketch__label--padparadscha { top: 55.6%;  left: 29%; font-size: 15px; }
.wg-sketch__label--star         { top: 69.6%;  left: 29%; font-size: 15px; }
.wg-sketch__label--colorchange  { top: 83.7%;  left: 29%; font-size: 15px; }
/* @raw-ok:end */

/* ---------- layout shell ---------- */
.wg-page { max-width: var(--layout-page-width); margin: 0 auto; }
.wg-section {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--layout-scroll-gap);
  padding: var(--layout-section-padding-y) var(--layout-margin);
}
.wg-scroll {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gap-block);
  width: 100%;
  /* landing offset when navigated to via the ToC (or any #anchor jump,
     or scroll-snap) — content starts 100px down from the viewport top,
     not flush against it; scroll-margin-top applies to snap alignment too */
  scroll-margin-top: var(--layout-section-padding-y);
  /* Continuous scroll-position-driven reveal: --reveal (0→1) is written on
     every scroll frame by js/case-study-scroll.js, a pure function of the
     block's current position — not a one-time IntersectionObserver trigger.
     Symmetric by construction: scrolling down through a block and back up
     through it play the identical curve in reverse, no state to get stuck.
     Combined effect (settled on after comparing skew/zoomstack/focus-rise
     separately): skew+inertia at full strength, plus light touches of
     zoomstack (scale) and focus-rise (blur) layered in. */
  opacity: var(--reveal, 0);
  filter: blur(calc((1 - var(--reveal, 0)) * 4px)); /* @raw-ok motion: reveal blur amount */
  transform:
    translateY(calc((1 - var(--reveal, 0)) * 140px))
    skewY(calc((1 - var(--reveal, 0)) * 9deg))
    scale(calc(0.96 + (0.04 * var(--reveal, 0)))); /* @raw-ok motion: reveal travel distance and skew */
}
@media (prefers-reduced-motion: reduce) {
  .wg-scroll { opacity: 1; filter: none; transform: none; }
}
/* No JS: --reveal is never written, so blocks would stay invisible. */
html:not(.js) .wg-scroll { opacity: 1; filter: none; transform: none; }
/* The reframe box (scroll area 6) is the one mandatory stop on the page —
   see js/case-study-scroll.js's reframe-expand IIFE. scroll-snap-stop:
   always forces the browser to land here even on a fast fling, unlike
   every other block's default (skippable) scroll-snap-align: start
   above. Centered, not top-aligned, since the expand/contract math is
   built around the box's own center staying fixed in the viewport —
   scroll-margin-top is zeroed here too, since the inherited 100px (meant
   for start-aligned blocks, so content doesn't sit flush at the viewport
   top) would otherwise skew the browser's actual landing position well
   away from true center, outside the JS's centered-detection tolerance.
   Prototyped and tuned in isolation first: experiments/reframe-expand/. */
#wg-reframe-scroll {
  scroll-margin-top: 0;
}
@media (min-width: 1280px) {
  .wg-scroll { scroll-snap-align: start; }
  #wg-reframe-scroll { scroll-snap-align: center; scroll-snap-stop: always; }
}
.wg-row { display: flex; gap: var(--gap-block); align-items: flex-start; width: 100%; }

/* @toc-tokens:start — every value in here must be a var(--…) token;
   scripts/check-tokens.sh fails the push otherwise. */
/* ---------- fixed ToC ---------- */
.wg-toc {
  position: fixed;
  top: var(--spacing-40);
  left: var(--spacing-40);
  z-index: var(--toc-z);
  transition: color var(--duration-600) var(--ease-out-quint);
  /* No accent in the ToC (D10 addendum): neutral on dark (14.2:1), white
     on accent zones (5.81:1). Dimmed dashes are set further down. */
  color: var(--toc-text);
}
.wg-toc.is-on-accent { color: var(--toc-text-on-accent); }
.wg-toc__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.wg-toc__item {
  display: flex;
  align-items: center;
  gap: var(--spacing-8);
  /* 24px, not Figma's 16px (expanded) / 12px (collapsed): WCAG 2.5.8 needs
     24x24 targets at rest. Touch has no hover, so the 32px hover state
     can't count (D10, option 2, 2026-10-07). */
  height: var(--toc-row);
  cursor: pointer;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  transition: height var(--duration-600) var(--ease-out-quint);
}
/* Collapsed variant: shorter rows, dashes sit closer together. Triggered
   from js/case-study-scroll.js as soon as active scroll target != "1"
   (i.e. as soon as area 2 is reached) and only removed on reaching area 1
   again — see updateActive(). */
.wg-toc.is-collapsed .wg-toc__item { height: var(--toc-row); }
/* Each row's real control: a link to its section (a11y A1). It takes over
   the row's flex layout so dash + label sit exactly as before. */
/* min-width: the collapsed (dash-only) link is a 24x24 target at rest
   without any visual change. */
.wg-toc__link { display: flex; align-items: center; gap: var(--spacing-8); height: 100%; min-width: var(--toc-row); color: inherit; text-decoration: none; }
/* Dock-style hover in collapsed mode: hovered item + its two immediate
   neighbors grow back to the expanded row height; only the hovered one
   (not the neighbors) reveals a label. is-hovered/is-near-hover are set
   in JS (mouseenter/mouseleave per item), not plain :hover, because the
   neighbor growth needs sibling awareness plain CSS :hover can't express. */
.wg-toc.is-collapsed .wg-toc__item.is-near-hover { height: var(--toc-row); } /* rest height is already 24px (2.5.8) */
/* Hovered row always grows to 32px, expanded or collapsed, primary or
   secondary — not just a label fade-in at a fixed row height. The
   collapsed-scoped selector is needed to out-specificity the
   .wg-toc.is-collapsed .wg-toc__item base rule above. */
.wg-toc__item.is-hovered,
.wg-toc.is-collapsed .wg-toc__item.is-hovered { height: var(--toc-row-hover); }

.wg-toc__dash {
  display: block;
  width: var(--spacing-8);
  height: var(--toc-dash);
  border-radius: var(--radius-full);
  background: currentColor;
  transition: width var(--duration-600) var(--ease-out-quint),
              background-color var(--duration-600) var(--ease-out-quint),
              opacity var(--duration-600) var(--ease-out-quint);
  flex-shrink: 0;
}
.wg-toc__item--labeled .wg-toc__dash { width: var(--spacing-12); }
.wg-toc__item.is-active .wg-toc__dash { width: var(--spacing-12); }
.wg-toc__item--labeled.is-active .wg-toc__dash { width: var(--spacing-16); }

/* Default state (Figma 779:920, updated 2026-10-07): every row that is
   neither active nor hovered — dash and label together — sits at 60% of the
   ToC colour, in both the expanded and collapsed versions. Neutral 60% on
   dark = 5.69:1. On accent zones the ToC colour is white, so 60% = 3.13:1
   (neutral 60% there would be 2.57:1, below the 3:1 a dash needs). Active
   and hovered rows are 100%. */
.wg-toc__link { transition: opacity var(--duration-600) var(--ease-out-quint); }
/* Focus ring: same as the page-wide :focus-visible rule, via ToC tokens */
.wg-toc__link:focus-visible { outline: var(--border-2) solid var(--toc-focus-ring); outline-offset: var(--focus-offset); }
.wg-toc__item:not(.is-active):not(.is-hovered) .wg-toc__link { opacity: var(--opacity-60); }
.wg-toc__label { /* text role label (active: label-strong), Figma text/label/default (D27, D34) */
  font: var(--type-label);
  color: currentColor;
  white-space: nowrap;
  overflow: hidden;
  max-width: var(--toc-label-max);
  opacity: 1;
  transition: max-width var(--duration-600) var(--ease-out-quint),
              opacity var(--duration-600) var(--ease-out-quint);
}
.wg-toc__item--labeled.is-active .wg-toc__label { font: var(--type-label-strong); }

/* Secondary labels (the 7 sub-items that don't carry a primary label):
   same caption scale as the primary labels, regular weight, not uppercase
   — hidden by default in both expanded and collapsed modes, revealed only
   on hover of that specific item. */
.wg-toc__label--secondary {
  font: var(--type-label);
  text-transform: none;
  max-width: 0;
  opacity: 0;
}
.wg-toc__item.is-hovered .wg-toc__label--secondary { max-width: var(--toc-label-max); opacity: 1; }

/* Collapsed mode: primary labels also hidden by default, revealed only on
   hover of that item (same mechanism as secondary labels above). */
.wg-toc.is-collapsed .wg-toc__label { max-width: 0; opacity: 0; }
.wg-toc.is-collapsed .wg-toc__item.is-hovered .wg-toc__label { max-width: var(--toc-label-max); opacity: 1; }

/* Hover: dash (+ label, where present) turn to the Figma neutral token,
   per ToC component's Hover variant. Driven by the JS-managed
   .is-hovered class instead of :hover, so it stays in sync with the
   dock-neighbor effect above using one source of truth. */
.wg-toc__item.is-hovered .wg-toc__dash { background: var(--toc-text); opacity: 1; }
.wg-toc__item.is-hovered .wg-toc__label { color: var(--toc-text); }

/* Labels follow the ToC colour (D10): neutral on dark, white on accent. */
.wg-toc__label { color: var(--toc-text); }
.wg-toc.is-on-accent .wg-toc__label { color: var(--toc-text-on-accent); }

/* ---------- ToC overlay, 781–1279px (Figma 948:3811; range widened for the iPad grid, 2026-10-09) ----------
   At these widths the side margin is only 80px, so revealed labels sat on
   top of the content (re-check N2). Here the ToC is always collapsed
   (js/case-study-scroll.js); pointer-enter or keyboard focus turns the same
   element into the overlay panel, anchored at the collapsed ToC's top-left.
   One element, so links, focus order and aria-current stay single.
   Rows: fixed 32px in every state, labels 16px (Figma 1088:8657, 2026-10-11). Default rows 60% (rule above), active
   and hovered 100%; hover only lengthens the dash (main 16px, sub 12px)
   and raises opacity. Text styles as in the other ToC variants. */
/* iPad portrait (781–1024): the page margin is 40px, so the collapsed rail
   sits inside it, 12px from the edge, clear of the content. */
@media (min-width: 781px) and (max-width: 1024px) {
  .wg-toc { left: var(--spacing-12); }
}
@media (min-width: 781px) and (max-width: 1279px) {
  .wg-toc.is-overlay-open {
    color: var(--toc-text); /* own dark panel, also over accent zones */
    background: var(--toc-surface);
    border: var(--border-1) solid var(--toc-border);
    border-radius: var(--radius-12);
    padding: var(--spacing-12) var(--spacing-40) var(--spacing-12) var(--spacing-12);
  }
  .wg-toc.is-overlay-open .wg-toc__label { color: var(--toc-text); } /* 16px: the label role's size at ≤1279 (D34) */
  /* Over an accent area the panel takes the accent (Ioli 2026-10-11). Rows go full white there:
     60% white on the accent is 3.1:1, below 4.5; the current row keeps its semibold label and long dash. */
  .wg-toc.is-overlay-open.is-on-accent { color: var(--toc-text-on-accent); background: var(--toc-surface-on-accent); }
  .wg-toc.is-overlay-open.is-on-accent .wg-toc__label { color: var(--toc-text-on-accent); }
  .wg-toc.is-overlay-open.is-on-accent .wg-toc__item .wg-toc__link { opacity: 1; }
  .wg-toc.is-overlay-open.is-on-accent { border-color: var(--toc-border-on-accent); }
  .wg-toc.is-overlay-open.is-collapsed .wg-toc__item,
  .wg-toc.is-overlay-open.is-collapsed .wg-toc__item.is-near-hover,
  .wg-toc.is-overlay-open.is-collapsed .wg-toc__item.is-hovered { height: var(--toc-row-overlay); transition: none; }
  .wg-toc.is-overlay-open.is-collapsed .wg-toc__label { max-width: var(--toc-label-max); opacity: 1; transition: none; }
  /* Active sub-item keeps the default 8px dash; only hover lengthens it */
  .wg-toc.is-overlay-open .wg-toc__item.is-active:not(.wg-toc__item--labeled):not(.is-hovered) .wg-toc__dash { width: var(--spacing-8); }
  /* On the accent there's no dimming, so the current row, sub-items too, is marked by weight and a
     longer dash (a11y review AB1, 2026-10-11); its link gives back the 4px width reservation. */
  .wg-toc.is-overlay-open.is-on-accent .wg-toc__item.is-active .wg-toc__label { font: var(--type-label-strong); } /* @raw-ok scoped: the overlay ToC exists only at 781–1279 */
  .wg-toc.is-overlay-open.is-on-accent .wg-toc__item.is-active:not(.wg-toc__item--labeled) .wg-toc__dash { width: var(--spacing-12); }
  /* No width jump: hover lengthens a dash by 4px, which widened the panel
     when the longest row ("Additional features") was hovered (188→192px).
     Every row whose dash isn't at its hover length reserves those 4px on
     its right, and hover hands them to the dash on the same curve, so the
     panel is as wide as its widest hovered row at all times. Applied in
     this whole width range (invisible while collapsed), not only while
     open, so opening the overlay doesn't animate the reservation in. */
  .wg-toc .wg-toc__link { transition: opacity var(--duration-600) var(--ease-out-quint), padding-right var(--duration-600) var(--ease-out-quint); }
  .wg-toc .wg-toc__item:not(.is-hovered):not(.wg-toc__item--labeled.is-active) .wg-toc__link { padding-right: var(--spacing-4); }
  .wg-toc.is-overlay-open.is-on-accent .wg-toc__item.is-active .wg-toc__link { padding-right: 0; } /* its dash took those 4px (AB1) */
}

/* (The Pause motion pill that lived here was removed on 2026-10-07 at
   Ioli's request; motion now follows the OS reduce-motion setting only. D10.) */
/* ≤480px (incl. 320px / 400% zoom): two stats side by side left ~92px each,
   too narrow for 24px words like "combinations" — stack them. */
@media (max-width: 480px) {
  .wg-row.wg-stats { flex-direction: column; }
}
/* Windows High Contrast / forced colours: backgrounds are replaced, so
   the dashes would vanish. Paint them in the system text colour, full
   strength; active/hover are still told apart by dash width. */
@media (forced-colors: active) {
  .wg-toc__dash,
  .wg-toc .wg-toc__item .wg-toc__dash { background: CanvasText !important; opacity: 1 !important; forced-color-adjust: none; }
  .wg-toc .wg-toc__item .wg-toc__link { opacity: 1 !important; }
}
.wg-toc__item--labeled.is-hovered .wg-toc__dash { width: var(--spacing-16); }
.wg-toc__item:not(.wg-toc__item--labeled).is-hovered .wg-toc__dash { width: var(--spacing-12); }

@media (prefers-reduced-motion: reduce) {
  .wg-toc, .wg-toc__dash, .wg-toc__item, .wg-toc__label { transition: none; }
}
/* @toc-tokens:end */

/* ---------- Back chip and ToC position (Figma "Header / Back" at 48, ToC at 132; D41) ----------
   The chip is the Foundations "Action chip" (base rules in css/ds.css). The header borrows the home page's header class for its
   layer only (--site-header-z); css/home.css is not loaded on a case-study page. */
.site-header { position: fixed; top: 48px; left: var(--spacing-40); z-index: var(--site-header-z); } /* @raw-ok measure: Figma Header / Back, y 48 */
.wg-toc { top: 132px; } /* @raw-ok measure: Figma Navigation / Section index, y 132: 48 + the 36px chip + 48 */
/* Phones (Figma 1518:5918, Ioli 2026-10-27): the header keeps a 60px row in the flow above the hero; the Home chip in it is fixed at the
   top left from the start and stays there, so it is still in place when the ToC reaches the top and takes it into its bar. Flush left as
   drawn, 44px high and without a background (.action-chip--touch, css/ds.css). */
@media (max-width: 780px) {
  .site-header { position: static; height: var(--size-top-bar); padding: 0; }
  .site-header a { position: fixed; top: calc(var(--size-top-bar) - var(--border-0-5) - var(--size-touch) - var(--spacing-8)); left: 0; z-index: calc(var(--site-header-z) + 1); }
  .site-header a:focus-visible { outline-offset: var(--focus-offset-inset); } /* the chip is flush left: an outer ring would be cut off by the screen edge */
  /* The Home chip has no fill of its own, so from load it sits on a bar of the page colour (review B1: it was white on white over the
     hero collage). The ToC's own bar covers it from the pin on. */
  .site-header::after { content: ""; position: fixed; top: 0; left: 0; right: 0; height: var(--size-top-bar); box-sizing: border-box; background: var(--surface-page); border-bottom: var(--border-0-5) solid var(--border-color-divider); z-index: var(--site-header-z); }
}

/* ---------- phone ToC, ≤780px (Figma section 1088:8935, Ioli 2026-10-11) ----------
   The rail is hidden on phones. Its place is taken by .wg-toc-m: a panel in
   the page under the hero ("On this page", the four main sections), which
   becomes a fixed compact bar with the current section once scrolled past,
   and opens from there with the current section highlighted. Behaviour:
   js/toc-phone.js. All values are tokens (Phase B, 2026-10-11). */
.wg-toc-m-slot { display: none; }
@media (max-width: 780px) {
  .wg-toc { display: none; }
  .wg-toc-m-slot {
    display: block; align-self: stretch;
    margin-inline: calc(-1 * var(--layout-margin)); /* full bleed, like the frame */
    margin-top: calc(-1 * var(--layout-scroll-gap)); /* the hero already ends with its section padding */
  }
  .wg-toc-m {
    background: var(--surface-raised);
    border-bottom: var(--border-0-5) solid var(--border-color-divider);
    color: var(--text-body);
  }
  /* pinned at the top from the first stop on; js/toc-phone.js sets the height while it closes */
  .wg-toc-m.is-pinned { position: fixed; top: 0; left: 0; right: 0; z-index: var(--toc-z); overflow: hidden; }
  .wg-toc-m.is-stuck { overflow: visible; }
  /* over an accent area (pinned or sticky) the bar takes the accent; the title goes white (neutral is 4.4:1 there) */
  .wg-toc-m.is-on-accent { background: var(--toc-surface-on-accent); }
  .wg-toc-m.is-on-accent .wg-toc-m__title { color: var(--text-body); }
  .wg-toc-m__toggle {
    display: flex; align-items: center; justify-content: space-between; gap: var(--gap-stack);
    width: 100%; padding: var(--inset-card) var(--inset-panel); /* the whole bar is the button */
    border: 0; background: none; color: inherit; font: inherit; cursor: pointer; text-align: left; /* @raw-ok scoped: the phone ToC exists only at ≤780 */
  }
  .wg-toc-m__toggle:focus-visible { outline: var(--border-2) solid var(--focus-ring); outline-offset: var(--focus-offset-inset); }
  .wg-toc-m__title { color: var(--text-secondary); }
  .wg-toc-m__current { font: var(--type-label); } /* @raw-ok scoped: the phone ToC exists only at ≤780 */
  .wg-toc-m__caret { width: var(--size-icon-20); height: var(--size-icon-20); flex: none; transform: rotate(180deg); }
  /* open: title + up chevron; closed: current section + down chevron */
  .wg-toc-m:not(.is-open) .wg-toc-m__title, .wg-toc-m.is-open .wg-toc-m__current, .wg-toc-m.is-open .wg-toc-m__sr { display: none; }
  .wg-toc-m.is-open .wg-toc-m__toggle { padding-bottom: calc(var(--gap-group) / 2); } /* the first link's padding adds the other half */
  .wg-toc-m:not(.is-open) .wg-toc-m__caret { transform: none; }
  .wg-toc-m__list { display: none; flex-direction: column; padding: 0 var(--inset-panel) calc(var(--gap-group) / 2); }
  .wg-toc-m.is-open .wg-toc-m__list { display: flex; }
  /* rows 22px, 20px apart (Figma): each link carries half the gap above and below, so every row is a real 42px target */
  .wg-toc-m__link {
    display: block; padding: calc(var(--gap-group) / 2) 0; color: var(--text-body); text-decoration: none;
    font: var(--type-label); /* @raw-ok scoped: the phone ToC exists only at ≤780 */
  }
  .wg-toc-m__link:focus-visible { outline: var(--border-2) solid var(--focus-ring); outline-offset: var(--focus-offset-inset); }
  .wg-toc-m.is-stuck .wg-toc-m__link.is-active {
    display: flex; align-items: center; min-height: var(--size-touch); margin-block: calc(var(--gap-group) / 2);
    padding: var(--inset-tight) var(--inset-compact); /* spacing scale (Ioli 2026-10-11; Figma had 6/10) */
    background: var(--toc-m-active-surface); border-radius: var(--toc-m-active-radius); font: var(--type-label-strong); /* @raw-ok scoped: the phone ToC exists only at ≤780 */
  }
  .wg-toc-m.is-on-accent.is-stuck .wg-toc-m__link.is-active { background: var(--toc-m-active-surface-on-accent); }

  /* From the pin on, the panel is one top bar (Figma 1518:6199, 6396): a 60px row with the Home chip (fixed, above) at the left and the
     toggle at the right, 44px high and without a background in every state (a tap leaves no fill behind). Pinned it reads "ON THIS PAGE ⌃";
     sticky, the current section and a caret. Opened, the list drops under the bar on the raised surface with its "On this page" heading. */
  .wg-toc-m.is-pinned { display: grid; grid-template-rows: calc(var(--size-top-bar) - var(--border-0-5)) auto; }
  .wg-toc-m.is-pinned .wg-toc-m__toggle {
    width: auto; justify-self: end; align-self: end; margin-bottom: var(--spacing-8); gap: var(--gap-tight);
    height: var(--size-touch); padding: var(--inset-tight) var(--inset-compact);
    border-radius: var(--radius-compact); background: none; -webkit-tap-highlight-color: transparent;
  }
  .wg-toc-m.is-pinned.is-open .wg-toc-m__toggle { padding-bottom: var(--inset-tight); }
  .wg-toc-m.is-pinned .wg-toc-m__toggle { transition: opacity var(--duration-200) var(--ease-fast-out-slow-in); }
  .wg-toc-m.is-pinned .wg-toc-m__toggle:active { opacity: var(--opacity-60); } /* the same press dim as the Home chip (no fill to change) */
  @media (prefers-reduced-motion: reduce) { .wg-toc-m.is-pinned .wg-toc-m__toggle { transition: none; } }
  .wg-toc-m.is-pinned .wg-toc-m__title { font: var(--type-caption-strong); text-transform: uppercase; color: var(--text-body); } /* @raw-ok scoped: the phone ToC exists only at ≤780 */
  .wg-toc-m.is-pinned:not(.is-stuck) .wg-toc-m__list { margin-top: calc(var(--spacing-60) - var(--size-top-bar) + var(--border-0-5) - var(--gap-group) / 2); } /* the list keeps the place it had in the page (the bar row is 10px taller than the page panel's title row) */
  .wg-toc-m.is-stuck { background: var(--surface-page); }
  .wg-toc-m.is-on-accent.is-stuck { background: var(--toc-surface-on-accent); }
  .wg-toc-m.is-stuck .wg-toc-m__title { display: none; }
  .wg-toc-m.is-stuck .wg-toc-m__current { display: inline; font: var(--type-caption-strong); text-transform: uppercase; } /* @raw-ok scoped: the phone ToC exists only at ≤780 */
  .wg-toc-m.is-stuck.is-open .wg-toc-m__panel { display: block; padding-top: var(--inset-card); background: var(--surface-raised); border-top: var(--border-0-5) solid var(--border-color-divider); }
  .wg-toc-m.is-on-accent.is-stuck.is-open .wg-toc-m__panel { background: var(--toc-surface-on-accent); }
  .wg-toc-m.is-stuck .wg-toc-m__sr { display: inline; } /* the name stays "On this page: <section>" open or closed */
  .wg-toc-m.is-on-accent.is-stuck.is-open .wg-toc-m__panel-title { color: var(--text-body); } /* white on the accent (the secondary text colour is 4.45:1 there) */
  .wg-toc-m.is-stuck.is-open .wg-toc-m__panel-title { display: block; min-height: var(--size-icon-20); margin-bottom: calc(var(--gap-group) / 2); padding-inline: var(--inset-panel); color: var(--text-secondary); }
}
.wg-toc-m__panel { display: contents; }
.wg-toc-m__panel-title { display: none; }


/* ---------- context section ---------- */
/* Hero composition — exact positions/sizes from Figma node 784:2151
   ("hero transition", Start/End frames): desktop mock centered (532x387,
   full height), GridCard left (120x264 at 85,102), MegaMenu right
   (400x206 at 522,102). Side items carry the Figma-annotated shadow
   (0 4 12 8, dark-surface @20%); the desktop mock doesn't.

   The entrance animation itself (each element on an independent curved
   path/easing, synced blur removal) is driven by js/case-study-scroll.js
   via the Web Animations API, not a CSS transition — a CSS transition can
   only interpolate linearly between two transform values (a straight
   line), which can't trace a curved path with its own speed profile.
   Prototyped and tuned in isolation first: experiments/hero-transition/.
   The transform/filter below are each element's t=0 (pre-animation) look,
   matching the JS's own first keyframe so there's no flash of the final
   state before the animation starts. */
/* @raw-ok:start artwork: hero composition and its browser-chrome mock, measured from Figma 784:2151; transforms are the entrance's start frame */
.wg-hero-visuals { position: relative; width: 100%; height: 387px; }

.wg-hero-visuals__desktop {
  position: absolute;
  left: 50%;
  top: 0;
  width: 532px;
  height: auto;
  border-radius: var(--radius-card);
  overflow: hidden;
  background: var(--surface-media);
  transform: translateX(-50%) translate(0px, 17.5px) scale(0.775);
  filter: blur(10px);
}
.wg-hero-visuals__toolbar { position: relative; height: 67px; background: var(--surface-media); }
.wg-hero-visuals__tabbar {
  position: absolute; left: 0; top: 0; width: 100%; height: 34px;
  background: #dedee0; display: flex; align-items: flex-end; gap: 2px;
  padding: 0 14px 0 76px;
}
.wg-hero-visuals__lights { position: absolute; left: 14px; top: 12px; width: 40px; height: 12px; }
.wg-hero-visuals__lights img { display: block; width: 100%; height: 100%; }
.wg-hero-visuals__tab {
  display: flex; align-items: center; gap: 6px; height: 24px; padding: 0 12px;
  background: var(--surface-media); border-radius: 10px 10px 0 0;
}
.wg-hero-visuals__tab img { width: 17px; height: 8px; display: block; }
.wg-hero-visuals__tab span { font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-size: 11px; color: #333; white-space: nowrap; }
.wg-hero-visuals__address {
  position: absolute; left: 0; top: 34px; width: 100%; height: 32px;
  background: var(--surface-media); display: flex; align-items: center; padding: 0 12px;
}
.wg-hero-visuals__address-field {
  flex: 1 0 0; height: 22px; border-radius: 14px; background: #f0f0f1;
  display: flex; align-items: center; padding: 0 12px;
  font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-size: 11px; color: #595959;
}
.wg-hero-visuals__address-field b { color: #333; font-weight: 400; }
.wg-hero-visuals__divider { position: absolute; left: 0; top: 66px; width: 100%; height: 1px; background: #d9d9d9; }

/* Screenshot is a full, much-taller page capture; cropped to its top
   320px here (Figma: scaled to container width, top-anchored, no other
   crop math needed — the exported PNG's native aspect already matches). */
.wg-hero-visuals__screenshot { position: relative; width: 100%; aspect-ratio: 532 / 320; overflow: hidden; }
.wg-hero-visuals__screenshot img { position: absolute; top: 0; left: 0; width: 100%; height: auto; display: block; }

.wg-hero-visuals__side {
  position: absolute;
  top: 102px;
  border-radius: var(--radius-card);
  box-shadow: var(--shadow-md);
  overflow: hidden;
}
.wg-hero-visuals__side img { width: 100%; height: 100%; object-fit: cover; display: block; }
.wg-hero-visuals__grid {
  left: 85px; width: 120px; height: 264px;
  transform: translate(65.42px, 53px) scale(0.757);
  filter: blur(10px);
}
.wg-hero-visuals__mega {
  left: 522px; width: 400px; height: 206px;
  transform: translate(-56.5px, 29px) scale(0.7175);
  filter: blur(10px);
}
/* @raw-ok:end */

@media (prefers-reduced-motion: reduce) {
  .wg-hero-visuals__desktop { transform: translateX(-50%); filter: none; }
  .wg-hero-visuals__grid, .wg-hero-visuals__mega { transform: none; filter: none; }
}
/* No JS: entrances never run, so show every "before entrance" element in
   its settled state (same values as the reduce-motion rules). */
html:not(.js) .wg-hero-visuals__desktop { transform: translateX(-50%); filter: none; }
html:not(.js) .wg-hero-visuals__grid, html:not(.js) .wg-hero-visuals__mega { transform: none; filter: none; }
html:not(.js) .wg-meta-card, html:not(.js) .wg-round__stats { transform: none; opacity: 1; }
html:not(.js) .wg-graph__value { opacity: 1; }

.wg-intro { display: flex; flex-direction: column; gap: var(--gap-group); width: 100%; }
/* The title leads in the source, so the page reads title first; from 781px the collage is placed above it (phones order everything explicitly below) */
@media (min-width: 781px) {
  .wg-scroll[data-scroll="1"] > .wg-hero-visuals, .wg-scroll[data-scroll="1"] > .ws-hero { order: -1; }
}

.wg-meta-card {
  display: flex;
  gap: var(--gap-block);
  padding: var(--inset-panel);
  border-radius: var(--radius-card);
  background: var(--surface-accent);
  width: 100%;
}
.wg-meta-card__col { flex: 1 0 0; min-width: 0; display: flex; flex-direction: column; gap: var(--gap-tight); align-items: center; }
.wg-meta-card__row { display: flex; gap: var(--gap-tight); align-items: center; justify-content: center; width: 100%; }
.wg-meta-card__icon { font-size: var(--type-icon-size); line-height: var(--type-icon-line-height); }
.wg-meta-card__value { text-align: center; width: 100%; }

/* ---------- meta-card icon animations ----------
   Play once, in sequence, after the box's own "magnet pull" entrance
   lands (chained via animationend in js/case-study-scroll.js) — hat
   waves, then desktop shines, then rocket flies in. Same one-per-session
   persistence as the box itself (piggybacks on its animationend, which
   only fires when the box actually played its entrance — never in the
   showSettled() fast path). Prototyped and tuned in isolation first:
   experiments/meta-card-icons/. */

/* hat — direct reuse of the home page's wave emoji (css/site.css:262-277,
   .wave-emoji/@keyframes wave), same values, just JS-triggered via a
   class instead of baked into the base rule. */
.wg-meta-card__icon--hat {
  display: inline-block;
  transform-origin: 70% 70%;
}
.wg-meta-card__icon--hat.is-waving {
  animation: wg-icon-wave 700ms ease-in-out 2; /* @raw-ok motion: icon choreography timing */
}
@keyframes wg-icon-wave {
  0%, 100% { transform: rotate(0deg); }
  25% { transform: rotate(14deg); }
  50% { transform: rotate(-10deg); }
  75% { transform: rotate(8deg); }
}

/* desktop — light-reflection shine sweep, clipped to just the screen
   (not the stand/base). Insets measured directly from the rendered glyph
   (a 1:1 element screenshot at a CSS transform scale, not a font-size
   blowup — that renders a different bitmap strike with different
   proportions and gave wrong numbers the first time round). `screen`
   blend mode, not `overlay` — overlay barely lightens the screen's
   near-black pixels.

   Originally built by moving the gradient bar itself via `transform:
   translateX`, with `clip-path` on that SAME moving element meant to
   mask it down to just the screen area. That doesn't actually work:
   clip-path's inset() percentages resolve against the element's OWN
   border box — the bar (40% width/140% height of the icon, translating
   every frame) — not the icon, so the "masked to the screen" window
   moved WITH the bar as one rigid unit instead of staying fixed over the
   screen. It only looked right at some animation frames by coincidence
   (confirmed by reproducing and fixing the same bug on the round-stats
   icons that reuse this pattern, scroll area 9 — stepping through frame
   by frame showed it drifting off the screen entirely at others). Fixed
   the same way here: the pseudo-element now IS the fixed screen window
   directly (top/left/width/height set to the measured screen region,
   resolved correctly against the icon as the containing block, never
   transformed), with the gradient's own `background-position` animated
   inside that fixed window instead. overflow:hidden is no longer needed
   either, now that the shine is self-contained by construction. */
.wg-meta-card__icon--desktop {
  position: relative;
  display: inline-block;
}
.wg-meta-card__icon--desktop::after {
  content: '';
  position: absolute;
  top: 20%;
  left: 3%;
  width: 85%;
  height: 41%;
  background: linear-gradient(115deg, transparent 20%, rgba(255, 255, 255, 0.85) 50%, transparent 80%); /* @raw-ok motion: shine sweep highlight */
  background-size: 300% 100%;
  background-position: 150% 0;
  mix-blend-mode: screen;
  pointer-events: none;
}
.wg-meta-card__icon--desktop.is-shining::after {
  animation: wg-icon-shine-sweep 850ms ease-out forwards; /* @raw-ok motion: icon choreography timing */
}

/* rocket — flies the last short distance up and to the right
   (bottom-left → top-right), per the reference the motion was checked
   against — the intended travel path, independent of which way the
   glyph's own nose happens to be tilted. Decelerating into rest, no
   overshoot — just arrives. */
.wg-meta-card__icon--rocket {
  display: inline-block;
}
.wg-meta-card__icon--rocket.is-arriving {
  animation: wg-icon-rocket-arrive var(--duration-500) cubic-bezier(0.2, 0.8, 0.2, 1) forwards; /* @raw-ok motion: icon choreography timing */
}
@keyframes wg-icon-rocket-arrive {
  0% { transform: translate(-10px, 10px); }
  100% { transform: translate(0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .wg-meta-card__icon--hat.is-waving,
  .wg-meta-card__icon--desktop.is-shining::after,
  .wg-meta-card__icon--rocket.is-arriving { animation: none; }
}

/* ---------- meta-card "magnet pull" entrance ----------
   Hidden until the user's first scroll gesture from the very top of the
   page (js/case-study-scroll.js — also gates on a sessionStorage flag so
   this plays at most once per browser session, never replays on a
   mid-page refresh), then pulled into place: an immediate, already-fast
   pull-in (no slow ramp-up — moves the instant it's triggered) that
   overshoots rest, swings back under it, overshoots a little again, then
   settles — like it snapped onto a magnet.
   Prototyped and tuned in isolation first: experiments/meta-card-reveal/.

   Opacity is a plain `transition`, deliberately separate from the bounce
   `@keyframes` below — a single monotonic fade that reaches 1 early and
   then just stays. Keeping it out of the keyframes avoids it sharing that
   animation's several different per-segment `animation-timing-function`s,
   which otherwise caused a visible dip even with matching 0/1 endpoints
   at each stage (confirmed while tuning the prototype). */
.wg-meta-card {
  transform: translateY(100vh);
  opacity: 0;
  transition: opacity var(--duration-500) ease-out;
}
.wg-meta-card.is-revealed {
  opacity: 1;
  animation: wg-magnet-pull var(--duration-1400) forwards;
}

@keyframes wg-magnet-pull {
  0% {
    transform: translateY(100vh);
    animation-timing-function: cubic-bezier(0.15, 0.6, 0.3, 1); /* immediate velocity, no ramp-up */
  }
  55% {
    transform: translateY(-20px); /* overshoots rest — the "snap" */
    animation-timing-function: ease-out;
  }
  74% {
    transform: translateY(9px); /* swings back under rest */
    animation-timing-function: ease-out;
  }
  89% {
    transform: translateY(-3px); /* small second overshoot */
    animation-timing-function: ease-out;
  }
  100% {
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .wg-meta-card { transform: none; opacity: 1; transition: none; animation: none; }
}

.wg-hint {
  display: flex;
  gap: var(--gap-tight);
  align-items: center;
  padding: var(--inset-compact) var(--inset-panel);
  border-radius: var(--radius-card);
  background: var(--surface-light);
  width: 100%;
}
.wg-hint p { color: var(--text-on-light); }
.wg-hint p:first-child { font-size: var(--type-icon-s-size); } /* the ℹ️ icon keeps its size at every width */ /* accent on neutral was 4.45:1 (a11y A9) */

.wg-split > .wg-visual { flex: 1 0 0; align-self: stretch; }
.wg-split__text { flex: 1 0 0; min-width: 0; display: flex; flex-direction: column; gap: var(--gap-stack); }
.wg-split__text .body-m, .wg-split__text .statement { color: var(--text-body); }
.wg-split__text .statement { color: var(--text-accent); }

/* ---------- floating filter chips ("the ask" visual) ---------- */
.wg-split > .wg-visual.chip-stage-host { display: flex; align-items: center; justify-content: center; background: transparent; }
.chip-stage { position: relative; width: 460px; height: 300px; flex-shrink: 0; margin: auto; } /* @raw-ok artwork: diagram stage, Figma frame size */

.chip {
  position: absolute;
  transform: translate(-50%, -50%) translate(var(--push-x, 0px), var(--push-y, 0px));
  /* Standalone `translate` (not `transform`) so the physics-driven drift
     below isn't caught by this rule's `transition: transform …` — that
     transition is meant for the occasional --push-x/--push-y state change,
     not a value rewritten every animation frame (which would smear each
     frame's target through a 550ms ease and read as laggy, not physical). */
  translate: var(--drift-x, 0px) var(--drift-y, 0px);
  font: var(--chip-font);
  color: var(--chip-text);
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--chip-gap);
  background: var(--chip-surface);
  border: var(--border-1) solid var(--chip-border);
  border-radius: var(--chip-radius);
  padding: var(--chip-padding-y) var(--chip-padding-x);
  box-shadow: var(--chip-glow-rest);
  /* @raw-ok:start motion: the chip's own spring choreography */
  transition: padding 550ms cubic-bezier(0.34, 1.2, 0.64, 1),
              border-radius 550ms cubic-bezier(0.34, 1.2, 0.64, 1),
              background var(--duration-600) var(--ease-out-quint),
              border-color var(--duration-600) var(--ease-out-quint),
              box-shadow var(--duration-600) var(--ease-out-quint),
              opacity var(--duration-600) var(--ease-out-quint),
              transform 550ms var(--ease-fast-out-slow-in);
  /* @raw-ok:end */
}
.chip.is-active {
  opacity: 1;
  background: var(--chip-surface-active);
  border-color: var(--chip-border-active);
  border-radius: var(--chip-radius-active);
  padding: var(--chip-padding-active-y) var(--chip-padding-active-x);
  box-shadow: var(--chip-glow-active);
  z-index: 1;
}

.chip-sub {
  position: absolute;
  font: var(--chip-font);
  color: var(--chip-text);
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--chip-gap);
  background: var(--chip-surface);
  border: var(--border-1) solid var(--chip-border);
  border-radius: var(--chip-radius);
  padding: var(--chip-padding-y) var(--chip-padding-x);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translate(var(--dx, 0px), var(--dy, 0px)) scale(0.4);
  transition: transform 800ms var(--ease-fast-out-slow-in), /* @raw-ok motion: subtype reveal timing */
              opacity 800ms var(--ease-fast-out-slow-in), /* @raw-ok motion: subtype reveal timing */
              visibility 0s linear 800ms; /* @raw-ok motion: subtype reveal timing */
}
.chip-sub.is-visible {
  opacity: 0.5; /* @raw-ok a11y-floor: min 0.46 for 4.5:1 label contrast (a11y A10) */
  visibility: visible;
  z-index: 1;
  transform: translate(0, 0) scale(1);
  transition: transform 800ms var(--ease-fast-out-slow-in), /* @raw-ok motion: subtype reveal timing */
              opacity 800ms var(--ease-fast-out-slow-in), /* @raw-ok motion: subtype reveal timing */
              visibility 0s linear 0s; /* @raw-ok motion: subtype reveal timing */
}

/* @raw-ok:start artwork: chip and subtype positions and stagger, measured from the Figma diagram */
.chip[data-family="quartz"]     { left: 60px;  top: 55px; }
.chip[data-family="jade"]       { left: 230px; top: 40px; }
.chip[data-family="garnet"]     { left: 400px; top: 55px; }
.chip[data-family="topaz"]      { left: 75px;  top: 140px; }
.chip[data-family="tourmaline"] { left: 385px; top: 140px; }
.chip[data-family="opal"]       { left: 110px; top: 230px; }
.chip[data-family="sapphire"]   { left: 230px; top: 210px; }
.chip[data-family="ruby"]       { left: 40px;  top: 265px; }

.chip[data-family="quartz"]     { animation-delay: 0s; }
.chip[data-family="jade"]       { animation-delay: 0.5s; }
.chip[data-family="garnet"]     { animation-delay: 1s; }
.chip[data-family="topaz"]      { animation-delay: 1.5s; }
.chip[data-family="tourmaline"] { animation-delay: 2s; }
.chip[data-family="opal"]       { animation-delay: 2.5s; }
.chip[data-family="sapphire"]   { animation-delay: 3s; }
.chip[data-family="ruby"]       { animation-delay: 3.5s; }

.chip-sub[data-parent="quartz"][data-label="Rose"]           { left: 110px; top: 57px;  --dx: -73px; --dy: -15px; }
.chip-sub[data-parent="quartz"][data-label="Smoky"]          { left: 69px;  top: 100px; --dx: -38px; --dy: -58px; }
.chip-sub[data-parent="quartz"][data-label="Rutile"]         { left: 2px;   top: 92px;  --dx: 33px; --dy: -50px; }
.chip-sub[data-parent="quartz"][data-label="Rock Crystal"]   { left: 1px;   top: -9px;  --dx: 10px; --dy: 51px; }
.chip-sub[data-parent="quartz"][data-label="Citrine"]        { left: 115px; top: 2px;   --dx: -83px; --dy: 40px; }

.chip-sub[data-parent="jade"][data-label="Jadeite"]          { left: 271px; top: 48px;  --dx: -73px; --dy: -21px; }
.chip-sub[data-parent="jade"][data-label="Nephrite"]         { left: 118px; top: 5px;   --dx: 77px; --dy: 22px; }

.chip-sub[data-parent="garnet"][data-label="Spessartite"]    { left: 370px; top: 92px;  --dx: -14px; --dy: -50px; }
.chip-sub[data-parent="garnet"][data-label="Demantoid"]      { left: 269px; top: 98px;  --dx: 88px; --dy: -56px; }
.chip-sub[data-parent="garnet"][data-label="Hessonite"]      { left: 273px; top: 21px;  --dx: 87px; --dy: 21px; }
.chip-sub[data-parent="garnet"][data-label="Rhodolite"]      { left: 384px; top: -9px;  --dx: -22px; --dy: 51px; }

.chip-sub[data-parent="topaz"][data-label="Imperial"]        { left: 120px; top: 148px; --dx: -79px; --dy: -21px; }

.chip-sub[data-parent="tourmaline"][data-label="Paraiba"]    { left: 394px; top: 177px; --dx: -41px; --dy: -50px; }
.chip-sub[data-parent="tourmaline"][data-label="Rubellite"]  { left: 307px; top: 193px; --dx: 42px; --dy: -66px; }
.chip-sub[data-parent="tourmaline"][data-label="Watermelon"] { left: 228px; top: 149px; --dx: 110px; --dy: -22px; }
.chip-sub[data-parent="tourmaline"][data-label="Chrome"]     { left: 295px; top: 77px;  --dx: 57px; --dy: 50px; }
.chip-sub[data-parent="tourmaline"][data-label="Bi-color"]   { left: 392px; top: 77px;  --dx: -40px; --dy: 50px; }

.chip-sub[data-parent="opal"][data-label="Black"]            { left: 150px; top: 237px; --dx: -65px; --dy: -20px; }
.chip-sub[data-parent="opal"][data-label="White"]            { left: 57px;  top: 272px; --dx: 27px; --dy: -55px; }
.chip-sub[data-parent="opal"][data-label="Boulder"]          { left: 66px;  top: 167px; --dx: 12px; --dy: 50px; }

.chip-sub[data-parent="sapphire"][data-label="Padparadscha"] { left: 284px; top: 222px; --dx: -109px; --dy: -25px; }
.chip-sub[data-parent="sapphire"][data-label="Star"]         { left: 143px; top: 245px; --dx: 66px; --dy: -48px; }
.chip-sub[data-parent="sapphire"][data-label="Color Change"] { left: 162px; top: 147px; --dx: 15px; --dy: 50px; }

.chip-sub[data-parent="ruby"][data-label="Star"]             { left: 81px;  top: 270px; --dx: -62px; --dy: -18px; }
/* @raw-ok:end */

@media (prefers-reduced-motion: reduce) {
  .chip, .chip-sub { transition: none; }
  .chip, .chip.is-active { animation: none; }
}

@media (max-width: 780px) {
  .chip-stage { transform: scale(min(1, calc((100vw - 2 * var(--layout-margin)) / 460px))); transform-origin: center; } /* @raw-ok artwork: diagram stage scaled to fit; 460 = stage width */
}

.wg-stats { color: var(--text-accent); }
.wg-stat { flex: 1 0 0; min-width: 0; display: flex; gap: var(--gap-group); align-items: flex-start; }
.wg-stat__number { white-space: nowrap; color: inherit; } /* type: h1 (D20) */
.wg-stat__label { flex: 1 0 0; min-width: 0; font: var(--type-statement); } /* type: statement */

.wg-context-detail__text { flex: 1 0 0; min-width: 0; color: var(--text-body); }

/* ---------- bar chart ("the data" visual) ----------
   Flat neutral fill + a 32x32 gemstone image pinned to the bar's current
   right edge, replacing the earlier frosted-glass treatment entirely —
   per Figma node 556:3635. Width + the count-up both come from one
   requestAnimationFrame loop in js/data-graph.js, off a single eased
   progress value, so they can't drift out of sync the way two
   separately-timed animations (a CSS transition + an independent JS
   count-up) would. Prototyped and tuned in isolation first:
   experiments/graph-bar-sapphire/. */
.wg-graph {
  width: 100%;
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: var(--gap-stack);
  row-gap: var(--gap-stack);
  align-items: center;
}
/* display: contents flattens each row into the grid, so label/track land
   in the 2 columns above instead of needing a fixed label width — the
   label column auto-sizes to its own widest entry */
.wg-graph__row { display: contents; }
.wg-graph__label {
  text-align: right;
  white-space: nowrap;
  color: var(--text-accent); /* #7e80ff on surface-dark, 5.7:1 (the old accent was 3.2:1, a11y A8); Ioli 2026-10-11 */
}
/* Bar + count live together in one flex track (the actual 1fr column),
   not the count in its own separate grid column — the count must ride
   immediately behind the bar's current right edge as it grows, which a
   fixed-width grid column can't express (it would just sit pinned at the
   far end of the full row regardless of the bar's current width). */
.wg-graph__track {
  display: flex;
  align-items: center;
  gap: var(--gap-stack);
  width: 100%;
}
.wg-graph__value {
  text-align: left;
  white-space: nowrap;
  color: var(--text-accent); /* same style and colour as the label (Ioli 2026-10-11) */
  flex-shrink: 0;
  /* Hidden until the bar starts moving — not just showing "0" at rest. */
  opacity: 0;
  transition: opacity var(--duration-200) ease-out;
}
.wg-graph__value.is-visible { opacity: 1; }

.wg-graph__bar {
  position: relative;
  height: var(--chart-thumb);
  width: var(--chart-thumb); /* rest state: exactly the image, nothing else visible */
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--surface-chart-bar);
}
.wg-graph__img {
  display: block;
  width: var(--chart-thumb);
  height: var(--chart-thumb);
  border-radius: var(--radius-card);
  flex-shrink: 0;
}

/* A bar narrower than its image: the image sits just past the bar's end,
   and the count follows the image (Figma 963:355; js/data-graph.js sets .is-short) */
.wg-graph { --wg-graph-thumb-gap: var(--spacing-8); }
.wg-graph__bar.is-short { overflow: visible; }
.wg-graph__bar.is-short .wg-graph__img { position: absolute; left: calc(100% + var(--wg-graph-thumb-gap)); top: 0; }
.wg-graph__bar.is-short + .wg-graph__value { margin-left: calc(var(--chart-thumb) + var(--wg-graph-thumb-gap)); }

@media (prefers-reduced-motion: reduce) {
  .wg-graph__bar, .wg-graph__value { transition: none; }
}

/* ---------- graph group + legend (reusable component — a top-bordered
   caption row for annotating a chart/visual; usable under any visual on
   any page, not tied to the bar chart specifically) ---------- */
.wg-graph-group { display: flex; flex-direction: column; gap: var(--spacing-16); width: 100%; }
.wg-legend { width: 100%; padding: var(--inset-tight) 0; border-top: var(--border-1) solid var(--border-color-divider); }

/* ---------- process section ---------- */
.wg-callout {
  padding: var(--inset-panel);
  border-radius: var(--radius-card);
  background: var(--surface-light);
  width: 100%;
}
.wg-callout p { color: var(--text-accent); }

.wg-lead .heading-m { width: var(--layout-third); flex-shrink: 0; align-self: stretch; }
.wg-lead__body { flex: 1 0 0; min-width: 0; display: flex; flex-direction: column; gap: var(--gap-stack); }
.wg-lead__body .body-m { color: var(--text-body); }

.wg-approaches__grid { display: flex; gap: var(--gap-block); width: 100%; }
/* Card (Figma 963:394, updated 2026-10-10): square sketch, then "SOLUTION A ❌" in one row,
   then the assessment. The DOM keeps name → sketch → mark → text; only the visual order moves. */
.wg-approach {
  flex: 1 0 0; min-width: 0;
  display: grid; grid-template-columns: auto 1fr; gap: var(--gap-stack); align-content: start;
}
.wg-approach .wg-visual { grid-column: 1 / -1; order: 0; width: 100%; }
@media (min-width: 781px) { /* square sketch frame filling the column (phones: fixed 197px frame, below) */
  .wg-approach .wg-visual { height: auto; aspect-ratio: 1 / 1; }
  .wg-approach .wg-sketch { justify-content: flex-start; } /* left-aligned if it ever stacks */
  .wg-approach .wg-sketch > .wg-sketch__svg { height: auto; aspect-ratio: 1 / 1; }
  .wg-approach .wg-sketch__canvas { height: auto; width: 100%; }
}
.wg-approach p.wg-approach__name { order: 1; align-self: center; color: var(--text-secondary); }
.wg-approach p.wg-approach__mark { order: 2; align-self: center; }
.wg-approach p.body-detail { order: 3; grid-column: 1 / -1; color: var(--text-body); }

/* ---------- reframe box — hard-stop + center-fixed expand ----------
   Two layers: .wg-reframe stays in normal flow at its natural size (so
   updateReveal() and the ToC's IntersectionObserver, which both key off
   [data-scroll="6"], keep working unmodified); .wg-reframe__frame is the
   thing that actually animates width/height (background, blobs,
   border-radius); .wg-reframe__content (h2 + body) is absolutely
   positioned, centered via transform, at a FIXED size — taken out of
   flow entirely, so it is structurally incapable of moving or resizing
   no matter what the frame does. The mandatory scroll stop
   (scroll-snap-stop: always, below) and the expand/contract trigger
   (js/case-study-scroll.js) are what's new; prototyped and tuned in
   isolation first: experiments/reframe-expand/. Rest-state dimensions
   (890x156) are measured directly off this page's own rendered box, not
   guessed. */
.wg-reframe { width: 100%; display: flex; justify-content: center; }

.wg-reframe__frame {
  position: relative;
  overflow: hidden;
  background: var(--surface-page);
  border-radius: var(--radius-card);
  width: var(--layout-content-width);
  height: 156px; /* @raw-ok measure: reframe rest height, read by the expand script */
  flex-shrink: 0; /* explicit width must not get clamped by the flex column's own max-width */
}

.wg-reframe__content {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: calc(var(--layout-content-width) - 2 * var(--inset-panel)); /* fixed — frame's rest width (890) minus its 24px*2 padding */
  display: flex;
  align-items: center;
  gap: var(--gap-block);
  z-index: 1;
}
.wg-reframe__content .heading-m { width: var(--layout-third); flex-shrink: 0; }
.wg-reframe__body { flex: 1 0 0; min-width: 0; display: flex; flex-direction: column; gap: 0; color: var(--text-body); }

.wg-mesh-bg__blob {
  position: absolute;
  z-index: 0;
  border-radius: 50%;
  filter: blur(48px); /* @raw-ok artwork: mesh blob softness */
  mix-blend-mode: screen;
  background: var(--surface-accent);
  pointer-events: none;
}
.wg-mesh-bg__blob--1 { width: 340px; height: 340px; top: -120px; left: -60px; opacity: 0.7; animation: wg-mesh-drift-1 9s ease-in-out infinite alternate; } /* @raw-ok artwork: mesh blob geometry and drift */
.wg-mesh-bg__blob--2 { width: 280px; height: 280px; bottom: -110px; right: -40px; opacity: 0.55; animation: wg-mesh-drift-2 12s ease-in-out infinite alternate; } /* @raw-ok artwork: mesh blob geometry and drift */
.wg-mesh-bg__blob--3 { width: 220px; height: 220px; top: 30px; left: 38%; opacity: 0.9; animation: wg-mesh-drift-3 7s ease-in-out infinite alternate; } /* @raw-ok artwork: mesh blob geometry and drift */

@keyframes wg-mesh-drift-1 {
  0%   { transform: translate(-22%, -16%) scale(1); }
  50%  { transform: translate(20%, 14%) scale(1.25); }
  100% { transform: translate(-10%, 22%) scale(0.9); }
}
@keyframes wg-mesh-drift-2 {
  0%   { transform: translate(18%, -14%) scale(1); }
  50%  { transform: translate(-22%, 10%) scale(0.8); }
  100% { transform: translate(8%, -20%) scale(1.15); }
}
@keyframes wg-mesh-drift-3 {
  0%   { transform: translate(-16%, 14%) scale(1); }
  50%  { transform: translate(20%, -16%) scale(1.2); }
  100% { transform: translate(-10%, -10%) scale(0.85); }
}
/* Pausable (D10, WCAG 2.2.2): the page-wide Pause toggle (js/motion-pref.js)
   sets html.motion-paused. With reduce-motion on, the blobs hold still
   from first paint — before JS runs — unless the visitor pressed Play.
   (Supersedes the earlier "always drift" request.) */
html.motion-paused .wg-mesh-bg__blob { animation-play-state: paused; }
/* Mesh blobs move only in 5s bursts (Ioli, 2026-10-07): each time the
   reframe box becomes the focused section, js/motion-pref.js adds
   .is-bursting for 5s. Otherwise, and without JS, they hold still. */
.wg-mesh-bg__blob { animation-play-state: paused; }
.wg-reframe__frame.is-bursting .wg-mesh-bg__blob { animation-play-state: running; }
html.motion-paused .wg-reframe__frame .wg-mesh-bg__blob { animation-play-state: paused; }

/* Demo videos (were GIFs until 2026-10-08). A video defaults to
   object-fit: contain, an img to fill; fill keeps the Figma crop
   percentages on these classes working exactly as they did for the GIFs. */
video.wg-app-shot__img, video.wg-s8-homepage__img, video.wg-round__media-img { object-fit: fill; }

/* Video play / pause buttons (js/demo-video.js; Ioli, 2026-10-07). Accent
   circles with white Phosphor icons (Bold): white on accent 5.81:1, accent
   against the white screenshots 4.45:1. Play shows whenever the video isn't
   playing. Pause exists only while it plays and is visible on hover (mouse),
   after a first tap (touch, .show-controls) or on keyboard focus; while
   invisible it can't be clicked, so a first tap only reveals it. */
.wg-video-btn {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 2;
  width: var(--video-btn-size); height: var(--video-btn-size); border: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--surface-accent); color: var(--text-body);
  cursor: pointer; pointer-events: auto;
}
.wg-video-btn[hidden] { display: none; }
.wg-video-btn svg { width: var(--size-icon-24); height: var(--size-icon-24); }
.wg-video-btn:focus-visible { box-shadow: 0 0 0 3px var(--focus-ring-on-light), 0 0 0 7px var(--focus-ring-on-light); } /* @raw-ok a11y: focus halo around the round video button */
.wg-video-btn--pause { opacity: 0; pointer-events: none; transition: opacity var(--duration-200) var(--ease-out-quint); }
@media (hover: hover) {
  .wg-video-host.is-playing:hover .wg-video-btn--pause { opacity: 1; pointer-events: auto; }
}
.wg-video-host.is-playing.show-controls .wg-video-btn--pause,
.wg-video-btn--pause:focus-visible { opacity: 1; pointer-events: auto; }
@media (prefers-reduced-motion: reduce) { .wg-video-btn--pause { transition: none; } }
@media (prefers-reduced-motion: reduce) {
  html:not(.motion-playing) .wg-mesh-bg__blob { animation-play-state: paused; }
}

/* ---------- solution section ---------- */
/* Full-bleed to the true viewport edge, not just out of the section's
   195px padding — .wg-page itself caps at 1280px max-width, so breaking
   out to only that boundary still left a visible gutter of plain
   surface-dark on any window wider than 1280px (confirmed: at 1728px
   viewport the pill sat at left:216.5/right:1496.5 instead of edge-to-edge).
   The 100vw + negative-margin trick breaks out of every ancestor's width
   constraint at once; padding re-applies the inset for the inner content. */
.wg-solution-pill {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  background: var(--surface-accent);
  border-radius: var(--radius-pill);
  padding: var(--layout-section-padding-y) 0;
  display: flex;
  justify-content: center;
}
/* content stays capped to the normal page column, centered within the
   full-bleed background rather than stretching to the true viewport width
   (.wg-solution-pill's `justify-content: center` centers this within it) */
.wg-solution-pill__inner {
  /* never wider than the normal content column, but also never wider than
     the viewport minus the current responsive side-margin at narrow sizes */
  width: min(var(--layout-content-width), calc(100% - (2 * var(--layout-margin))));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--layout-scroll-gap);
}

/* ---------- area 7: before/after drag-reveal ---------- */
.wg-ba-centre { align-self: center; display: flex; flex-direction: column; gap: var(--gap-stack); width: var(--layout-content-width); max-width: 100%; }
.wg-ba-pills { position: relative; width: 100%; height: 35px; } /* @raw-ok measure: Before/After pill height from Figma */
/* crisp, no blur/blend — the "After" pill slides+squash-stretches toward the
   "Before" pill with spring easing and sits above it (z-index), covering it
   as it arrives; "Before" fades out on the same curve so the takeover reads
   clean at the very end. No merge/metaball effect — see
   feedback_match-figma-exactly / the 2025-10-01 pill-animation revision for
   why a blurred goo filter was dropped in favour of this.
   Neutral background + accent text, per Figma node 558:3646 (previously
   accent background + white text) — the glossy top-edge inset highlight
   is dropped too, since it was tuned to read against the old dark accent
   fill and is essentially invisible on this light neutral one. */
.wg-ba-pill-blob {
  position: absolute;
  top: 0;
  height: 35px; /* @raw-ok measure: Before/After pill height from Figma */
  border-radius: var(--radius-pill);
  background: var(--surface-light);
}
.wg-ba-pill-blob--before {
  left: 0;
  width: 77px; /* @raw-ok artwork: Before/After pill geometry and spring */
  opacity: calc(1 - pow(var(--pull, 0), 4));
  transition: opacity 260ms ease; /* @raw-ok artwork: Before/After pill geometry and spring */
}
.wg-ba-pill-blob--after {
  width: 68px; /* @raw-ok artwork: Before/After pill geometry and spring */
  z-index: 1;
  left: calc((100% - 68px) * (1 - var(--pull, 0))); /* @raw-ok artwork: Before/After pill geometry and spring */
  transform: scaleX(calc(1 + 0.32 * sin(var(--pull, 0) * 3.14159)));
  transform-origin: right center;
  transition: left 260ms cubic-bezier(.34,1.56,.64,1), transform 220ms cubic-bezier(.4,1.6,.6,1), opacity var(--duration-200) ease; /* @raw-ok artwork: Before/After pill geometry and spring */
}
.wg-ba-pills.is-popped .wg-ba-pill-blob--after { transform: scale(0); opacity: 0; }
.wg-ba-pill-label {
  position: absolute;
  top: 0;
  height: 35px; /* @raw-ok measure: Before/After pill height from Figma */
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-on-light); /* a11y A9: accent on neutral was 4.45:1 */
  pointer-events: none;
}
.wg-ba-pill-label--before {
  left: 0;
  width: 77px; /* @raw-ok artwork: Before/After pill geometry and spring */
  opacity: calc(1 - pow(var(--pull, 0), 4));
  transition: opacity 260ms ease; /* @raw-ok artwork: Before/After pill geometry and spring */
}
.wg-ba-pill-label--after {
  width: 68px; /* @raw-ok artwork: Before/After pill geometry and spring */
  z-index: 1;
  left: calc((100% - 68px) * (1 - var(--pull, 0))); /* @raw-ok artwork: Before/After pill geometry and spring */
  transition: left 260ms cubic-bezier(.34,1.56,.64,1), transform 220ms cubic-bezier(.4,1.6,.6,1), opacity var(--duration-200) ease; /* @raw-ok artwork: Before/After pill geometry and spring */
}
.wg-ba-pills.is-popped .wg-ba-pill-label--after { transform: scale(0); opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .wg-ba-pill-blob, .wg-ba-pill-label { transition: none; }
}
/* Figma 1127:2225: two halves; in each, the app shot beside its caption and
   text, 12px apart. The shots take their height from their own aspect-ratio,
   never from the row (align-items: flex-start): a stretched flex item ignores
   aspect-ratio, and after rotating an iPad Safari kept the old row height and
   squashed the screenshots. Equal widths, so both shots are equally tall
   (Figma draws Before 7px narrower, 199.5 vs 206.5, from a fixed text width). */
.wg-compare-detail { display: flex; gap: var(--gap-block); width: 100%; align-items: flex-start; }
.wg-compare-detail__col { flex: 1 0 0; min-width: 0; display: flex; gap: var(--gap-stack); align-items: flex-start; }
.wg-compare-detail__col > .wg-app-shot, .wg-compare-detail__text { flex: 1 0 0; min-width: 0; }
.wg-compare-detail__text { display: flex; flex-direction: column; gap: var(--gap-stack); }
/* App screenshots: an oversized, top-anchored image inside an overflow:hidden
   frame, percentages matching the Figma node exactly. */
.wg-app-shot {
  position: relative;
  width: 100%;
  aspect-ratio: 496 / 1080;
  border-radius: var(--radius-20);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
}
.wg-app-shot__img { position: absolute; left: 0; top: 0; width: 100%; height: 100%; max-width: none; pointer-events: none; }
.wg-app-shot__img--crop-top { top: 0.03%; height: 102.27%; }
.wg-ba {
  position: relative;
  width: 100%;
  aspect-ratio: 890 / 556;
  border-radius: var(--radius-20);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  background: var(--surface-media);
  touch-action: pan-y; /* vertical swipes scroll the page; horizontal drags move the split */
  cursor: ew-resize;
}
.wg-ba__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
  pointer-events: none;
  user-select: none;
}
.wg-ba__img--before { clip-path: inset(0 calc(100% - var(--split, 50%)) 0 0); }
.wg-ba__line {
  position: absolute;
  top: 0;
  left: var(--split, 50%);
  transform: translateX(-50%);
  width: 6px; /* @raw-ok artwork: slider divider width */
  height: 100%;
  background: var(--border-color-divider);
  pointer-events: none;
}
.wg-ba__handle {
  position: absolute;
  top: 50%;
  left: var(--split, 50%);
  transform: translate(-50%, -50%);
  width: 95px; /* @raw-ok artwork: slider handle size */
  height: 95px; /* @raw-ok artwork: slider handle size */
  pointer-events: none;
}

/* ---------- area 8: mega-menu + PDP/filters redesign ---------- */
.wg-s8-homepage { flex: 1 0 0; min-width: 0; position: relative; aspect-ratio: 580 / 375; border-radius: var(--radius-card); overflow: hidden; box-shadow: var(--shadow-md); }
.wg-s8-row1__text { width: var(--layout-third); flex: none; display: flex; flex-direction: column; gap: var(--gap-stack); color: var(--text-body); }
.wg-s8-pdp-col { display: flex; flex-direction: column; gap: var(--gap-block); width: var(--layout-two-thirds); flex: none; }
.wg-s8-filters-col { position: relative; display: flex; flex: 1 0 0; min-width: 0; }
.wg-s8-visual { position: relative; width: 100%; padding: var(--inset-panel); overflow: hidden; }
.wg-s8-visual--pdp { height: auto; aspect-ratio: 580 / 475; border-radius: var(--radius-card); box-shadow: var(--shadow-sm); }
.wg-s8-visual--filters { height: auto; aspect-ratio: 270 / 559; border-radius: var(--radius-card) var(--radius-card) 0 0; }
/* Figma 1127:5619 / 975:6038: 8px accent stroke, 8/8 dashes, centred on the
   bottom edge (on the column, outside the panel's overflow clip) */
.wg-s8-filters-col::after { content: ''; position: absolute; left: 0; right: 0; bottom: calc(-1 * var(--spacing-4)); height: var(--spacing-8); background: repeating-linear-gradient(to right, var(--surface-accent) 0 var(--spacing-8), transparent var(--spacing-8) var(--spacing-16)); pointer-events: none; }
.wg-s8-visual__img, .wg-s8-homepage__img { position: absolute; left: 0; width: 100%; max-width: none; pointer-events: none; }
.wg-s8-homepage__img { top: -0.41%; height: 100.63%; }
.wg-s8-visual--pdp .wg-s8-visual__img { top: -0.02%; height: 211.78%; }
.wg-s8-visual--filters .wg-s8-visual__img { top: -0.25%; height: 752.24%; }

.wg-validation__intro .body-m { color: var(--text-body); }
.wg-rounds { display: flex; gap: var(--gap-block); width: 100%; }
.wg-round { flex: 1 0 0; min-width: 0; display: flex; flex-direction: column; gap: var(--gap-stack); }
/* Figma 425 x 371; never stretched: the box keeps its proportion and fills
   the column. The screenshot fills the box exactly (cover) instead of
   overflowing it: iPad Safari stopped clipping the overflowing image during
   the reveal and drew its bottom over the page (2026-10-12). object-position
   reproduces Figma's crop: the image 2.5% above the box top (round 1),
   1.17% (round 2), i.e. y = offset / (image height / box height - 1). */
.wg-round__media { position: relative; width: 100%; aspect-ratio: 425 / 371; border-radius: var(--radius-card); overflow: hidden; }
.wg-round__media-img { position: absolute; inset: 0; width: 100%; height: 100%; max-width: none; object-fit: cover; pointer-events: none; }
.wg-round__media-img--round1 { object-position: 50% 1.66%; }
.wg-round__media-img--round2 { object-position: 50% 4.84%; }
/* Round 2: two app screens side by side, an 8px seam of page colour between
   them (Figma: a 4px stroke on each inner edge) */
.wg-round__media--pair { display: flex; }
.wg-round__media-half { position: relative; flex: 1 0 0; min-width: 0; overflow: hidden; }
.wg-round__media--pair::after { content: ''; position: absolute; top: 0; bottom: 0; left: 50%; width: var(--spacing-8); transform: translateX(-50%); background: var(--surface-page); }
.wg-round__stats {
  display: flex;
  gap: var(--gap-group);
  padding: var(--spacing-16);
  border-radius: var(--radius-card);
  background: var(--surface-accent);
  text-align: center;
}
.wg-round__stat { flex: 1 0 0; min-width: 0; display: flex; flex-direction: column; gap: var(--gap-tight); align-items: center; color: var(--text-body); }
.wg-round__stat .wg-round__icon { font-size: var(--type-icon-size); line-height: var(--type-icon-line-height); }
.wg-round p.body-detail { color: var(--text-body); }
.wg-round strong { font-weight: var(--type-strong-weight); }

/* ---------- round-stats entrance + icon animations ----------
   Same "magnet pull" mechanism as .wg-meta-card (scroll area 1) — reuses
   its exact @keyframes wg-magnet-pull, just applied to these two frames —
   then an icon sequence chained via animationend: face (both) together,
   then mobile/desktop (both) together, then bullseye/popper (both)
   together. Each frame runs its own independent chain; since both frames
   share identical durations/easings, they stay in lockstep naturally,
   with no cross-frame sync code needed. Trigger is scroll-into-view
   (IntersectionObserver, js/case-study-scroll.js) — not the "first
   scroll from page top" gating used for the meta-card, since this section
   sits far down the page. Built directly on the live page (no isolated
   prototype round for this one, per instruction). */
.wg-round__stats {
  transform: translateY(100vh);
  opacity: 0;
  transition: opacity var(--duration-500) ease-out;
}
.wg-round__stats.is-revealed {
  opacity: 1;
  animation: wg-magnet-pull var(--duration-1400) forwards;
}
@media (prefers-reduced-motion: reduce) {
  .wg-round__stats { transform: none; opacity: 1; transition: none; animation: none; }
}

/* face — a gentle "nod": small scale pulse + head-tilt wiggle, inspired by
   Noto Emoji Animation's slightly-happy-face motion (an original
   interpretation, not the literal Google asset — this project doesn't
   embed a Lottie player for a couple of icon flourishes). */
.wg-round__icon--face {
  display: inline-block;
  transform-origin: 50% 85%;
}
.wg-round__icon--face.is-nodding {
  animation: wg-icon-face-nod 700ms ease-in-out 2; /* @raw-ok motion: icon choreography timing */
}
@keyframes wg-icon-face-nod {
  0%, 100% { transform: rotate(0deg) scale(1); }
  30% { transform: rotate(-6deg) scale(1.08); }
  60% { transform: rotate(4deg) scale(1.04); }
}

/* mobile + desktop — light-reflection shine sweep, same visual idea as
   the meta-card's desktop icon (scroll area 1) but a corrected technique:
   that original version moved the gradient bar itself via `transform:
   translateX`, with `clip-path` on that SAME moving element meant to
   mask it down to just the screen area — but clip-path's inset()
   percentages resolve against the element's OWN border box, which was
   the bar (40% width/140% height of the icon, translating every frame),
   not the icon. So the "masked to the screen" window moved WITH the bar
   as one rigid unit instead of staying fixed over the screen — it only
   looked right by coincidence at some animation frames and was visibly
   wrong at others (confirmed by stepping through the animation frame by
   frame). Fixed here by making the pseudo-element BE the fixed screen
   window directly (top/left/width/height set to the measured screen
   region, resolved correctly against the icon as the containing block,
   never transformed) and animating the gradient's own `background-
   position` inside that fixed window instead — the standard, correct
   way to sweep a reflection across a masked region. Left the original
   meta-card desktop icon exactly as shipped (different keyframe name
   below) rather than risk regressing it.

   Neither `overflow: hidden` nor padding is needed on the icon anymore
   either, now that the shine is self-contained by construction — both
   were only there to stop the old translating bar from escaping, and
   mobile's padding specifically to stop that same overflow:hidden from
   clipping the phone glyph's own overshooting ink. Removing both also
   fixes a second bug: that padding was inflating the icon's layout box,
   which pushed its "Mobile" label down out of alignment with its
   siblings' labels. */
.wg-round__icon--mobile,
.wg-round__icon--desktop {
  position: relative;
  display: inline-block;
}
.wg-round__icon--mobile::after,
.wg-round__icon--desktop::after {
  content: '';
  position: absolute;
  background: linear-gradient(115deg, transparent 20%, rgba(255, 255, 255, 0.85) 50%, transparent 80%); /* @raw-ok motion: shine sweep highlight */
  background-size: 300% 100%;
  background-position: 150% 0;
  mix-blend-mode: screen;
  pointer-events: none;
}
/* fixed screen-region windows — same measured values as before, now used
   as direct position/size (correctly relative to the icon) instead of a
   clip-path on a translating element. */
.wg-round__icon--mobile::after { top: 18.8%; left: 38.2%; width: 29.4%; height: 59.3%; }
.wg-round__icon--desktop::after { top: 20%; left: 3%; width: 85%; height: 41%; }
.wg-round__icon--mobile.is-shining::after,
.wg-round__icon--desktop.is-shining::after {
  animation: wg-icon-shine-sweep 850ms ease-out forwards; /* @raw-ok motion: icon choreography timing */
}
@keyframes wg-icon-shine-sweep {
  0% { background-position: 150% 0; }
  100% { background-position: -150% 0; }
}

/* bullseye — an impact "hit": quick punch-in then an overshoot spring back
   out, plus a small shake, inspired by Noto Emoji Animation's direct-hit
   motion (original interpretation, not the literal asset). */
.wg-round__icon--bullseye {
  display: inline-block;
}
.wg-round__icon--bullseye.is-hit {
  animation: wg-icon-bullseye-hit var(--duration-600) cubic-bezier(0.3, 0, 0.2, 1) forwards; /* @raw-ok motion: icon choreography timing */
}
@keyframes wg-icon-bullseye-hit {
  0% { transform: scale(1) rotate(0deg); }
  30% { transform: scale(0.8) rotate(0deg); }
  55% { transform: scale(1.2) rotate(6deg); }
  75% { transform: scale(0.95) rotate(-3deg); }
  100% { transform: scale(1) rotate(0deg); }
}

/* popper — a confetti "burst": fast scale-up overshoot with a quick
   rotate, settling back to rest, inspired by Noto Emoji Animation's
   party-popper motion (original interpretation, not the literal asset). */
.wg-round__icon--popper {
  display: inline-block;
}
.wg-round__icon--popper.is-popping {
  animation: wg-icon-popper-pop 650ms var(--ease-out-quint) forwards; /* @raw-ok motion: icon choreography timing */
}
@keyframes wg-icon-popper-pop {
  0% { transform: scale(1) rotate(0deg); }
  40% { transform: scale(1.35) rotate(-14deg); }
  70% { transform: scale(0.95) rotate(6deg); }
  100% { transform: scale(1) rotate(0deg); }
}

@media (prefers-reduced-motion: reduce) {
  .wg-round__icon--face.is-nodding,
  .wg-round__icon--mobile.is-shining::after,
  .wg-round__icon--desktop.is-shining::after,
  .wg-round__icon--bullseye.is-hit,
  .wg-round__icon--popper.is-popping { animation: none; }
}

/* ---------- outcomes section ---------- */
/* Outcomes block (Figma 947:3649): heading, lead and the two-column row
   sit 12px apart (sm), not the usual 40px between a block's children. */
.wg-scroll[data-scroll="10"] { gap: var(--gap-stack); }
.wg-outcomes-intro { display: flex; flex-direction: column; gap: var(--gap-stack); width: 100%; }
/* Figma: the outcomes section ends with 200px bottom padding (2 x the
   100px section padding; scales with it on narrow screens). */
.wg-section[data-section="outcomes"] { padding-bottom: calc(2 * var(--layout-section-padding-y)); }
.wg-tradeoffs { width: 100%; display: flex; gap: var(--gap-block); }
.wg-tradeoffs__col { flex: 1 0 0; min-width: 0; display: flex; flex-direction: column; gap: var(--gap-stack); }
.wg-tradeoffs__col .body-m { color: var(--text-body); }
.wg-search-input {
  display: flex;
  align-items: center;
  gap: var(--gap-tight);
  padding: 0 var(--inset-compact);
  border-radius: var(--radius-pill);
  background: var(--surface-light);
  width: 220px; /* @raw-ok measure: search bar width from Figma */
  height: 43px; /* @raw-ok measure: search bar height from Figma */
  flex-shrink: 0;
}
.wg-search-input__icon { font-size: var(--type-icon-size); line-height: var(--type-icon-line-height); }
.wg-search-input__value {
  font: var(--search-input-font); /* 16/150% on phone (tokens.css) */
  color: var(--text-on-light);
  opacity: 0.65; /* min 0.61 for 4.5:1 on neutral (a11y A11) */ /* @raw-ok a11y-floor: min 0.61 for 4.5:1 on neutral */
  transition: opacity var(--duration-200) var(--ease-out-quint);
}
.wg-search-input__value.is-filled { opacity: 1; }
.wg-search-input__cursor {
  display: none;
  width: 1px; /* @raw-ok artwork: typing caret */
  height: 14px; /* @raw-ok artwork: typing caret */
  margin-left: 1px; /* @raw-ok artwork: typing caret */
  background: var(--surface-accent);
  animation: search-cursor-blink 1s step-start infinite; /* @raw-ok motion: caret blink */
}
.wg-search-input__cursor.is-visible { display: inline-block; }
@keyframes search-cursor-blink { 50% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .wg-search-input__value { transition: none; }
  .wg-search-input__cursor { animation: none; display: none !important; }
}
.wg-tradeoffs__quote { flex: 1 0 0; min-width: 0; }

/* Sign-off (Figma 1129:6764): the callout, then the shipped landing page
   (two thirds), a gutter apart. The app phone sits on the callout's right,
   centred on it and overhanging top and bottom; the right padding keeps the
   text clear of it (Figma: text 540 = 890 - 40 - 310, the 310 being the
   phone's 43 inset + 194 width + 73 gap). */
.wg-signoff { display: flex; flex-direction: column; align-items: flex-start; gap: var(--layout-gutter); width: 100%; }
.wg-outcomes-card {
  position: relative;
  width: 100%;
  background: var(--surface-light);
  color: var(--text-on-light);
  border-radius: var(--radius-card);
  padding: var(--inset-wide);
  padding-right: 310px; /* @raw-ok measure: Figma 1129:6764 phone inset 43 + width 194 + 73 gap to the text */
  display: flex;
  flex-direction: column;
  gap: var(--gap-block);
}
.wg-outcomes-card__phone { position: absolute; right: 43px; top: 50%; transform: translateY(-50%); width: 194px; aspect-ratio: 194 / 435; } /* @raw-ok measure: Figma 1129:6745 phone box 194 x 435, 43 from the right */
/* The video (968 x 2160) is a hair narrower than Figma's box: cover crops it, never stretches */
.wg-outcomes-card__phone video { object-fit: cover; }
.wg-signoff__page { width: var(--layout-two-thirds); aspect-ratio: 580 / 940; border-radius: var(--radius-card); overflow: hidden; box-shadow: var(--shadow-md); }
.wg-signoff__page img { display: block; width: 100%; height: auto; }
.wg-outcomes-card__body { display: flex; flex-direction: column; gap: var(--gap-stack); }
.wg-outcomes-card .heading-m { color: var(--text-on-light); }

/* ---------- stack transition: outcomes (11) -> next case study ----------
   "stack" scroll effect (ref: alvarotrigo.com/fullPage/scroll-effects/,
   the "stack" entry) — the outgoing section stays pinned in place while
   the incoming one slides up from below and covers it, rather than both
   scrolling away together. Scoped to ONLY this one boundary, not the
   other 10 scroll-snap areas: area 11's .wg-scroll wrapper is the thing
   that stays put (position: sticky; top: 0), and .wg-next — already the
   next full-bleed, min-height:100vh section in normal flow right after
   it — naturally slides up over it as the page scrolls, with no extra
   JS needed. z-index makes the cover direction explicit rather than
   relying on default paint order. This also plays nicely, unintentionally,
   with the existing --reveal system (js/case-study-scroll.js): once stuck,
   this block's rect stops changing, so `exit` in updateReveal() never
   drops below 1 — the card simply stays fully visible/un-faded while
   covered, instead of fading out first, which is the desired look. */
/* Area 11 is taller than a screen since the sign-off visuals (2026-10-12):
   it pins once its bottom reaches the viewport bottom, so all of it is seen
   before the next section covers it. js/case-study-scroll.js measures
   --pin-top = min(0, viewport height - area height). */
.wg-scroll[data-scroll="11"] {
  position: sticky;
  top: var(--pin-top, 0);
  z-index: 1;
}

/* ---------- next section (full viewport, full-bleed background) ---------- */
@media (min-width: 1280px) { .wg-next { scroll-snap-align: start; scroll-snap-stop: always; } }
.wg-next {
  position: relative;
  z-index: 2;
  /* scroll-snap-type on <html> is y proximity (loose, by design, for the
     11 regular scroll-focus areas) — too loose for this one boundary,
     where resting mid-cover reads as broken rather than intentional.
     start + stop:always forces a decisive commit to the fully-covered
     state once the user scrolls into this zone, the same mandatory-stop
     treatment already used for the reframe box (area 6, #wg-reframe-scroll
     above) for the same reason: a transition with its own choreography
     needs a hard landing point, not proximity's "settle wherever." */
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  background: var(--surface-accent) var(--gradient-angular-accent) 0 0 / 100% 100% no-repeat; /* Figma 963:512, 1489:4118: the angular accent gradient */
  min-height: 100vh;
  padding: var(--inset-wide) 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
/* content stays capped to the page's own width, centered within the
   full-bleed background rather than stretching to the true viewport width */
.wg-next__inner {
  width: calc(min(var(--layout-page-width), 100%) - 2 * var(--inset-wide)); /* Figma 963:513: 1200 at x 40, the inset kept above 1280 too */
}
.wg-next-row { display: flex; gap: var(--gap-block); width: 100%; align-items: stretch; }
.wg-next__text { flex: 1 0 0; min-width: 0; display: flex; flex-direction: column; gap: var(--gap-stack); justify-content: center; }
.wg-next__text .caption-strong { color: var(--text-body); }
.wg-next__link { color: inherit; text-decoration: none; }
.wg-next .wg-visual { flex: 1 0 0; min-height: 400px; } /* @raw-ok measure: next-case preview minimum height */
/* Next-case cover (Figma 1489:4123, 1489:4146): a 600 x 400 panel with a collage. The pieces are placed with
   --x --y --w --h in Figma pixels of the 600-wide panel and scale with it (container units), clipped by the panel */
.wg-next .wg-next__cover { position: relative; display: block; overflow: hidden; container-type: inline-size; align-self: flex-start; aspect-ratio: 3 / 2; min-height: 0; background: var(--surface-page); }
.wg-next__piece { position: absolute; display: block; left: calc(var(--x) * 100cqw / 600); top: calc(var(--y) * 100cqw / 600); width: calc(var(--w) * 100cqw / 600); height: calc(var(--h) * 100cqw / 600); overflow: hidden; border-radius: var(--radius-card); box-shadow: var(--shadow-md); } /* @raw-ok measure: Figma panel is 600 wide */
.wg-next__piece img { position: absolute; display: block; left: 0; top: 0; width: 100%; height: 100%; max-width: none; object-fit: cover; pointer-events: none; }
/* Hover (Figma 1492:4317): the panel turns to the raised surface, the collage stays as it is. The cover and the title are
   separate links (the cover is out of the tab order and hidden from assistive tech), so hovering the title does not touch the cover. */
.wg-next .wg-next__cover { transition: background-color var(--duration-200) var(--ease-fast-out-slow-in); }
.wg-next .wg-next__cover:hover { background: var(--surface-raised); }
@media (prefers-reduced-motion: reduce) {
  .wg-next .wg-next__cover { transition: none; }
}
.wg-next__piece--dialog img { left: -0.01%; top: 0.06%; width: 100.01%; height: 131.55%; object-fit: fill; } /* @raw-ok measure: Figma crop of the search dialog */
.wg-next__piece--shot img { height: 162.17%; top: -0.05%; object-fit: fill; } /* @raw-ok measure: Figma crop of the full landing page */

/* ---------- responsive ---------- */
/* Phones: the stacks and the static hero (these were ≤1024 until the iPad grid). */
@media (max-width: 780px) {
  .wg-split, .wg-context-detail.wg-row, .wg-s8-row1, .wg-rounds, .wg-tradeoffs, .wg-s8-row2, .wg-approaches__grid {
    flex-direction: column;
  }
  .wg-context-detail__text, .wg-s8-row1__text, .wg-s8-pdp-col, .wg-s8-filters-col { width: 100%; }
  .wg-s8-homepage { flex: none; width: 100%; }
  .wg-hero-visuals { height: auto; }
  /* html + parent bump specificity past the html:not(.js) settled-state rule,
     whose translateX(-50%) is desktop-only geometry. */
  html .wg-hero-visuals .wg-hero-visuals__desktop { position: static; width: 100%; transform: none; filter: none; }
  .wg-hero-visuals.is-revealed .wg-hero-visuals__desktop { transform: none; }
  .wg-hero-visuals__grid, .wg-hero-visuals__mega { display: none; }
  .wg-stat { flex-direction: column; gap: var(--gap-tight); }
}

/* ---------- iPad grid (Ioli's Figma frames, 2026-10-09) ----------
   6 columns, 20px gutters; margins 40px in portrait (781–1024) and 80px in
   landscape (1025–1279). A desktop third spans 2 columns, a half 3. Equal
   flex children with a gutter between them land on the column lines by
   construction: with W = 6c + 5g, a half is 3c + 2g and a third 2c + g. */
@media (min-width: 781px) and (max-width: 1279px) {
  .wg-row, .wg-rounds, .wg-tradeoffs, .wg-approaches__grid, .wg-next-row,
  .wg-compare-detail { column-gap: var(--layout-gutter); }
  /* Before / After: two halves, 3 columns each */
  /* Identifying the problem: heading 2 columns, text 4 */
  .wg-lead .heading-m { width: calc((100% - 2 * var(--layout-gutter)) / 3); }
  /* Full-width blocks span all 6 columns */
  .wg-ba-centre { width: 100%; }
  .wg-next__inner { width: calc(100% - 2 * var(--layout-margin)); }
  /* Hero: the desktop composition (890 x 387) scaled to the 6 columns;
     js/case-study-scroll.js sets the zoom from the column width. */
  .wg-hero-visuals { width: 890px; } /* @raw-ok artwork: desktop hero composition width (js HERO_W) */
  /* the ask: the chip stage (460px artwork) is zoomed to its half by JS, so the halves stay on the grid */
  .wg-split > .wg-visual { min-width: 0; }
  /* Validation: the two rounds share their rows (subgrid), so the label,
     the text, the screenshot, the accent box and the closing text start on
     the same lines; a row is as tall as the taller of the two. */
  .wg-rounds { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 0; }
  .wg-round { display: grid; grid-row: span 5; grid-template-rows: subgrid; row-gap: var(--gap-stack); }
  .wg-round__media { align-self: start; } /* height from its aspect-ratio, not the shared row (same reason as the Before/After shots) */
  .wg-round__stats { padding: var(--inset-compact); }
    /* Compensating row 2: grid placement, per orientation below */
  .wg-s8-row2 { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); column-gap: var(--layout-gutter); row-gap: var(--gap-block); align-items: start; }
  .wg-s8-pdp-col { display: contents; }
}
/* Desktop (≥1280px), same two alignments as the iPad (Ioli, 2026-10-09):
   - Validation rounds share their rows (subgrid), so screenshot, accent box
     and closing text start on the same lines and the accent boxes are
     equally tall.
   - Compensating row 2 (Figma 963:457, 2026-10-12): paragraph across the
     full width, then the PDP (2 thirds) and the filter panel in the last
     third, tops level. Sizes stay 580 / 270. */
@media (min-width: 1280px) {
  .wg-rounds { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 0; }
  .wg-round { display: grid; grid-row: span 5; grid-template-rows: subgrid; }
  .wg-round__media { align-self: start; }
  .wg-s8-row2 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: var(--gap-block); row-gap: var(--gap-block); align-items: start; }
  .wg-s8-pdp-col { display: contents; }
  .wg-s8-pdp-col > .body-m { grid-column: 1 / -1; grid-row: 1; }
  .wg-s8-visual--pdp { grid-column: 1 / 3; grid-row: 2; }
  .wg-s8-filters-col { grid-column: 3 / 4; grid-row: 2; }
  /* Next case study (Figma 1489:4119, 963:513): destination copy 560, cover 600, 40 apart */
  .wg-next__text { flex: 560 0 0; } /* @raw-ok artwork: Figma column split 560 / 600 */
  .wg-next .wg-visual { flex: 600 0 0; } /* @raw-ok artwork: Figma column split 560 / 600 */
}
/* Landscape */
@media (min-width: 1025px) and (max-width: 1279px) {
  /* Compensating row 1: video 4 columns, heading + text 2 */
  .wg-s8-row1__text { width: calc((100% - 2 * var(--layout-gutter)) / 3); }
  /* Row 2: paragraph 6 columns; PDP in 1–4 and filter panel in 5–6, tops aligned */
  .wg-s8-pdp-col > .body-m { grid-column: 1 / 7; grid-row: 1; }
  .wg-s8-visual--pdp { grid-column: 1 / 5; grid-row: 2; }
  .wg-s8-filters-col { grid-column: 5 / 7; grid-row: 2; }
}
/* Portrait */
@media (min-width: 781px) and (max-width: 1024px) {
  .wg-split, .wg-context-detail.wg-row, .wg-s8-row1, .wg-tradeoffs {
    flex-direction: column;
  }
  .wg-context-detail__text, .wg-s8-row1__text { width: 100%; }
  .wg-stat { flex-direction: column; gap: var(--gap-tight); }
  /* Compensating row 1: heading + text, then the video, each 6 columns */
  .wg-s8-homepage { flex: none; width: 100%; }
  .wg-s8-row1__text { order: -1; }
  /* Row 2: paragraph 6 columns, then PDP 1–4 and filter panel 5–6, tops aligned */
  .wg-s8-pdp-col > .body-m { grid-column: 1 / 7; grid-row: 1; }
  .wg-s8-visual--pdp { grid-column: 1 / 5; grid-row: 2; }
  .wg-s8-filters-col { grid-column: 5 / 7; grid-row: 2; }
}
/* Reframe box below 1280px: the frame is 890px with 842px text at 1280px and
   up. Narrower, it flows: the frame spans the whole column (all 6 grid columns on iPad), keeps the 24px
   padding and the text size, and grows downward as the text wraps (iPad
   2026-10-09; first needed at 781–889px for a11y A15). The expand mechanic is
   gated to ≥1280px in js/case-study-scroll.js (its 1040px state needs the room). */
@media (max-width: 1279px) {
  .wg-reframe__frame { width: 100% !important; height: auto !important; padding: var(--inset-panel); }
  .wg-reframe__content {
    position: static;
    transform: none;
    width: 100%;
    flex-direction: column;
    align-items: flex-start;
  }
  .wg-reframe__content .heading-m { width: 100%; }
  /* heading to text: the same 12px as every heading + text pair (e.g. "What didn't work.") */
  .wg-reframe__content { gap: var(--gap-stack); }
}
@media (max-width: 780px) {
  /* Mobile layout (Figma 978:7286): 4-column grid, 24px margins, 60px
     section padding and 60px between a section's blocks. */
  .wg-lead, .wg-tradeoffs, .wg-meta-card, .wg-next-row {
    flex-direction: column;
  }
  .wg-lead .heading-m { width: 100%; }
  .wg-approaches__grid { flex-direction: column; }
  .wg-sketch__label { font-size: 13px; } /* @raw-ok artwork: sketch labels on phone */
  .wg-sketch__label--sapphire { font-size: 17px; } /* @raw-ok artwork: sketch labels on phone */
}

/* ==========================================================================
   Mobile layout pass (≤780px) — Figma 978:7286 "Case Study / Gemstones /
   Mobile" (Ioli, 2026-10-08). Values are Figma's; spec in the brief's
   "Mobile layout" section. Elements added for mobile only are hidden on
   desktop by the first rule, so ≥781px renders exactly as before.
   ========================================================================== */
.wg-hero-visuals__mobile, .chip-stage-m, .wg-compare-detail__wide-m, .wg-s8-mega-m { display: none; }

@media (max-width: 780px) {

  /* --- hero (area 1): title → collage → summary → meta card, 20px apart --- */
  /* Hero and Context are separate frames in Figma (each 60px padding), so
     the hero ends 60px earlier than a plain 60px block gap would give. */
  .wg-scroll[data-scroll="1"] { gap: var(--gap-group); margin-bottom: var(--layout-section-padding-y); }
  .wg-scroll[data-scroll="1"] .wg-intro { display: contents; }
  .wg-scroll[data-scroll="1"] .heading-l { order: 0; }
  /* Figma 978:7337: the collage bleeds to both screen edges (390 x 245) */
  .wg-scroll[data-scroll="1"] .wg-hero-visuals { order: 1; width: calc(100% + 2 * var(--layout-margin)); margin-inline: calc(-1 * var(--layout-margin)); aspect-ratio: 390 / 245; height: auto; }
  .wg-scroll[data-scroll="1"] .body-intro { order: 2; }
  .wg-scroll[data-scroll="1"] .wg-meta-card { order: 3; }
  html .wg-hero-visuals .wg-hero-visuals__desktop { display: none; }
  .wg-hero-visuals__mobile { display: block; position: absolute; inset: 0; width: 100%; height: 100%; max-width: none; object-fit: cover; object-position: top; }
  /* Corners on phone (Ioli, 2026-10-11): hero images 6, project visuals and graph bars 8,
     boxes 12, the Solution surface 24. Brief: "Phone guardrails". */
  .wg-visual, .wg-app-shot, .wg-round__media { border-radius: var(--radius-compact); }
  /* Entrance start state: the desktop zoom + unblur at phone size (same start
     scale 0.775 / 0.7175 and blur 10px, offsets scaled by the size ratio:
     collage 390/532, mega 233/400). js/case-study-scroll.js plays it.
     Mega-menu at Figma's 133, 37 / 233 x 120 in the 390 x 245 collage. */
  .wg-hero-visuals__mobile { transform: translate(0, 12.83px) scale(0.7744); filter: blur(10px); } /* @raw-ok artwork: phone hero entrance start frame */
  html .wg-hero-visuals .wg-hero-visuals__mega {
    display: block; left: 34.103%; top: 15.102%; width: 59.744%; height: auto; aspect-ratio: 1160 / 597;
    border-radius: var(--radius-6);
    box-shadow: var(--shadow-md); transform: translate(-32.91px, 16.89px) scale(0.7175); filter: blur(10px); /* @raw-ok artwork: phone hero entrance start frame */
  }
  @media (prefers-reduced-motion: reduce) {
    .wg-hero-visuals__mobile, html .wg-hero-visuals .wg-hero-visuals__mega { transform: none; filter: none; }
  }
  html:not(.js) .wg-hero-visuals__mobile, html:not(.js) .wg-hero-visuals .wg-hero-visuals__mega { transform: none; filter: none; }
  .wg-meta-card { flex-direction: column; gap: var(--gap-stack); padding: var(--inset-card); }
  .wg-meta-card__col { display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--gap-group); align-items: center; }
  .wg-meta-card__row { justify-content: flex-start; }
  .wg-meta-card__value { text-align: left; }

  /* --- context (area 2): hint → heading + text → chip diagram → highlight --- */
  .wg-scroll[data-scroll="2"] { gap: var(--gap-section); }
  .wg-hint { align-items: flex-start; padding: var(--inset-card); }
  .wg-split { gap: 0; }
  .wg-split__text { display: contents; }
  .wg-split__text .heading-m { order: 1; }
  .wg-split__text .body-m { order: 2; margin-top: var(--gap-stack); }
  .wg-split > .wg-visual.chip-stage-host { order: 3; margin-top: var(--gap-block); align-self: stretch; }
  .wg-split__text .statement { order: 4; margin-top: var(--gap-block); }
  .chip-stage { display: none; }
  .chip-stage-m { display: block; position: relative; width: 342px; height: 210px; margin: 0 auto; } /* Figma box; zoomed to fit below 390px (js/case-study-scroll.js) */ /* @raw-ok artwork: chip diagram box, Figma 342x210 */
  .chip-m {
    position: absolute; left: var(--x); top: var(--y);
    font: var(--chip-font); /* @raw-ok scoped: the phone chip diagram exists only at ≤780 */
    color: var(--chip-text); background: var(--chip-surface); border: var(--border-1) solid var(--chip-border); border-radius: var(--chip-radius);
    white-space: nowrap; padding: var(--chip-padding-active-y) var(--chip-padding-active-x);
    animation: wg-chip-float 5s ease-in-out var(--d, 0s) infinite alternate; /* @raw-ok motion: chip float */
  }
  .chip-m--family { opacity: var(--opacity-60); }
  .chip-m--sub { padding: var(--chip-padding-y) var(--chip-padding-x); }
  .chip-m--active { background: var(--chip-surface-active); border-color: var(--chip-border-active); }

  /* --- data (area 3): 2-column metrics, chart rows with label + count above the bar --- */
  .wg-row.wg-stats { flex-direction: row; gap: var(--gap-group); }
  .wg-stat { gap: var(--gap-hair); }
  .wg-stat__label { font: var(--type-body-s); color: var(--text-body); } /* @raw-ok one-off: the stat label switches role, statement → body-s, on phone (Figma) */
  .wg-graph-group { gap: var(--chart-row-gap); }
  .wg-graph { display: flex; flex-direction: column; align-items: stretch; gap: var(--chart-row-gap); }
  .wg-graph__row { display: block; position: relative; padding-top: calc(var(--chart-label-row) + var(--chart-label-gap)); }
  .wg-graph__label { position: absolute; left: 0; top: 0; line-height: var(--chart-label-row); text-align: left; } /* @raw-ok layout: line height sets the chart label row */
  .wg-graph__track { gap: 0; }
  .wg-graph__value { position: absolute; right: 0; top: 0; line-height: var(--chart-label-row); } /* @raw-ok layout: line height sets the chart label row */
  .wg-graph__bar { height: var(--chart-thumb-compact); width: var(--chart-thumb-compact); border-radius: var(--radius-compact); }
  .wg-graph__img { width: var(--chart-thumb-compact); height: var(--chart-thumb-compact); border-radius: var(--radius-compact); }
  .wg-graph { --wg-graph-thumb-gap: var(--spacing-4); }
  .wg-legend .caption-legend { color: var(--text-body); opacity: var(--opacity-60); }
  .wg-context-detail.wg-row { gap: var(--gap-stack); }

  /* --- process (areas 4–6) --- */
  .wg-scroll[data-scroll="4"] { gap: var(--gap-section); }
  .wg-callout { padding: var(--inset-card); }
  .wg-lead { gap: var(--gap-stack); }
  /* What didn't work: cards scroll sideways, bleeding past both margins to the screen edges */
  .wg-approaches__grid {
    position: relative; /* the containing block of the cards' absolutely positioned .sr-only text, so the scroller clips it (without it the page was 669px wide at 390) */
    flex-direction: row; gap: var(--gap-group);
    width: calc(100% + 2 * var(--layout-margin)); margin-inline: calc(-1 * var(--layout-margin)); padding-inline: var(--layout-margin);
    scroll-padding-inline: var(--layout-margin); /* a card rests at the page margin, the one before it runs off the screen edge */
    overflow-x: auto; scroll-snap-type: x mandatory; overscroll-behavior-x: contain; scrollbar-width: none;
  }
  .wg-approaches__grid::-webkit-scrollbar { display: none; }
  .wg-approaches__grid:focus-visible { outline-offset: var(--focus-offset-inset); }
  .wg-approach {
    flex: 0 0 252px; scroll-snap-align: start; /* @raw-ok measure: rejected-approach card width from Figma */
    display: grid; grid-template-columns: auto 1fr; column-gap: var(--gap-tight); row-gap: var(--gap-stack); align-content: start;
  }
  .wg-approach .wg-visual { grid-column: 1 / -1; order: 0; height: 197px; background: var(--surface-page); border-radius: var(--radius-compact); } /* no frame, like desktop: only the sketch */ /* @raw-ok measure: sketch frame height from Figma */
  .wg-approach p.wg-approach__name { order: 1; align-self: center; color: var(--text-body); }
  .wg-approach p.wg-approach__mark { order: 2; }
  .wg-approach p.body-detail { order: 3; grid-column: 1 / -1; }
  /* The reframe: Figma's angular gradient replaces the drifting blobs here.
     The PNG is the gradient rendered in box-relative space (experiments/
     feature-gradient/gen.py), so stretching it matches Figma at any size. */
  .wg-reframe__frame { padding: var(--inset-card); background: var(--gradient-angular-accent) 0 0 / 100% 100% no-repeat; }
  .wg-reframe__frame .wg-mesh-bg__blob { display: none; }
  .wg-reframe__content { gap: var(--gap-stack); }

  /* --- solution (areas 7–8): the accent block itself, r24, 60/24 padding --- */
  .wg-section[data-section="solution"] { padding-top: 0; }
  /* Radius is scroll-driven (js/case-study-scroll.js writes --pill-radius): 24 outside, 0 once the pill fills the screen */
  .wg-solution-pill { border-radius: var(--pill-radius, var(--radius-24)); /* Figma r24, unbound: no radius token for 24 */ }
  .wg-solution-pill__inner, .wg-solution-pill .wg-scroll[data-scroll="7"], .wg-solution-pill .wg-scroll[data-scroll="8"] { gap: var(--gap-section); }
  .wg-solution-pill .heading-m.heading--on-accent { color: var(--text-secondary); }
  .wg-ba-centre { display: none; }
  /* Before / After (Figma 975:5913): the desktop pages at 448 x 280, wider
     than the screen; Before is right-aligned to the column and runs off the
     left edge, After starts at the column and runs off the right edge
     (body clips the overflow). Caption and text below each. */
  .wg-compare-detail { flex-direction: column; container-type: inline-size; }
  .wg-compare-detail__col { flex: none; width: 100%; flex-direction: column; }
  .wg-compare-detail__col > .wg-app-shot { display: none; }
  .wg-compare-detail__text { flex: none; width: 100%; }
  /* Width: Figma's 448 on a 390 screen, and always at least column + margin,
     so on wider phone-layout screens it still reaches the screen edge. */
  .wg-compare-detail__wide-m { display: block; position: relative; flex: none; width: max(448px, 100cqw + var(--layout-margin)); max-width: none; aspect-ratio: 448 / 280; overflow: hidden; border-radius: var(--radius-compact); } /* @raw-ok measure: Figma 975:5915 bleed image width */
  .wg-compare-detail__wide-m--before { align-self: flex-end; }
  .wg-compare-detail__wide-m img { position: absolute; display: block; height: auto; max-width: none; }
  /* Figma leaves 17.86% of the box empty on the left, all of it off screen at
     390; the gap never exceeds the part that's off screen, so it never shows. */
  .wg-compare-detail__wide-m--before img { left: min(17.86%, max(0px, 448px - 100cqw - var(--layout-margin))); top: -0.63%; width: 112.31%; } /* @raw-ok measure: Figma 975:5915 bleed image width */
  .wg-compare-detail__wide-m--after img { left: 0; top: -0.66%; width: 100%; }
  .wg-s8-row1 { gap: var(--gap-block); container-type: inline-size; }
  .wg-s8-homepage { display: none; }
  .wg-s8-row1__text { order: 0; }
  /* Mega-menu (Figma 1129:7043): 550 x 280 from the column, running off the right edge */
  .wg-s8-mega-m { display: block; order: 1; align-self: flex-start; width: max(550px, 100cqw + var(--layout-margin)); max-width: none; height: auto; aspect-ratio: 550 / 280; object-fit: cover; object-position: top; border-radius: var(--radius-compact); box-shadow: var(--shadow-md); } /* @raw-ok measure: Figma 1129:7043 bleed image width */
  /* Parallel redesigns (Figma 978:7638): text, the filter panel centred,
     then the PDP right-aligned to the column and running off the left edge */
  .wg-s8-row2 { gap: var(--spacing-24); container-type: inline-size; }
  .wg-s8-pdp-col { display: contents; }
  .wg-s8-pdp-col > .body-m { order: 0; }
  .wg-s8-filters-col { order: 1; flex: none; align-self: center; width: 200px; } /* @raw-ok measure: Figma 975:6037 filter panel width */
  .wg-s8-visual--filters { padding: 0; aspect-ratio: 200 / 435; border-radius: var(--radius-compact) var(--radius-compact) 0 0; }
  .wg-s8-visual--filters .wg-s8-visual__img { height: auto; }
  .wg-s8-visual--pdp { order: 2; flex: none; align-self: flex-end; width: max(448px, 100cqw + var(--layout-margin)); padding: 0; aspect-ratio: 448 / 280; border-radius: var(--radius-compact); box-shadow: var(--shadow-md); } /* @raw-ok measure: Figma 1129:7045 bleed image width */
  .wg-s8-visual--pdp .wg-s8-visual__img { left: min(8.52%, max(0px, 448px - 100cqw - var(--layout-margin))); top: -0.7%; height: auto; } /* @raw-ok measure: Figma 1129:7045 bleed image width */

  /* --- validation (area 9): rounds as raised cards, results as a vertical list --- */
  .wg-round { background: var(--surface-raised); border-radius: var(--radius-card); padding: var(--inset-compact); gap: var(--gap-stack); }
  .wg-round > .caption-heading { color: var(--text-body); }
  .wg-round > .body-m { color: var(--text-body); }
  .wg-round__media { max-width: none; aspect-ratio: 318 / 275; }
  .wg-round__media-img--round2 { object-position: 50% 5.25%; } /* Figma 1127:2759: 1.33% above the top */
  /* Figma 1127:2758: a 3px stroke of card colour on each inner edge */
  .wg-round__media--pair::after { width: 6px; background: var(--surface-raised); } /* @raw-ok measure: Figma 1127:2758 seam, 2 x 3px */
  .wg-round__stats { flex-direction: column; gap: var(--gap-stack); padding: var(--inset-card); text-align: left; }
  .wg-round__stat { flex-direction: row; gap: var(--gap-tight); align-items: center; }
  #validation > .statement { margin-top: calc(var(--gap-section) - var(--gap-block)); }

  /* --- outcomes (areas 10–11) --- */
  .wg-section[data-section="outcomes"] { padding-bottom: var(--layout-section-padding-y); }

  /* --- scrolling (Ioli, 2026-10-27): the browser's default, no snapping or gliding (to be reworked). --bar-h is the top bar's
     measured height (js/case-study-scroll.js): the scroll padding keeps nav targets clear of it. */
  /* body's overflow-x: hidden would make the body its own scroll container.
     clip trims the same overflow without creating one. */
  body { overflow-x: clip; }
  /* The bleeding visuals (hero, Before/After, mega-menu, PDP, sign-off page)
     run past the screen edges on purpose; clipping at main keeps them out of
     the page's scrollable width (body's clip alone left html at 669px). */
  .wg-page { overflow-x: clip; }
  html { scroll-padding-top: var(--bar-h, var(--size-top-bar)); }
  /* a nav target lands with 60px above it, clear of the bar */
  .wg-scroll { scroll-margin-top: var(--gap-section); scroll-margin-bottom: var(--gap-section); }
  /* Nav targets are measured from the transformed box, so the desktop
     reveal's 140px rise / skew / scale made areas land off their mark.
     On phones the reveal is fade + unblur only. */
  .wg-scroll { transform: none; }
  /* The desktop stack effect pins area 11 while the next section covers it; phones scroll it normally. */
  .wg-scroll[data-scroll="11"] { position: static; }
  .wg-tradeoffs { gap: var(--gap-section); }
  .wg-tradeoffs__col { gap: var(--gap-block); }
  .wg-search-input { width: 100%; height: auto; padding: var(--inset-compact); gap: var(--gap-stack); }
  /* Sign-off (Figma 978:7681): text full width, then the app phone in the
     card's flow, overhanging the card's bottom edge (the card ends 31.5% of
     the way down the phone); the landing page bleeds to both screen edges,
     60px below. 100cqw = the sign-off width, so the phone width is
     100cqw - 2 x 40 and both overhangs scale with it. */
  /* Phone width: the card's text width (Figma), capped so the whole phone
     fits the screen under the top bar with the 60px tall-area margins
     (a wide phone layout, e.g. a narrow desktop window, made it taller than
     the screen). Both overhangs scale with that width. */
  /* --wg-signoff-phone-w: the width of the phone mockup artwork (.wg-outcomes-card__phone), not a breakpoint (D32) */
  .wg-signoff { gap: var(--gap-section); container-type: inline-size; --wg-signoff-phone-w: min(100cqw - 2 * var(--inset-wide), (100svh - var(--bar-h, var(--size-top-bar)) - 2 * var(--gap-section)) * 262 / 569); } /* @raw-ok measure: Figma 1129:6779 phone ratio 262 x 569 */
  .wg-outcomes-card { padding: var(--inset-wide); margin-bottom: calc(var(--wg-signoff-phone-w) * 1.4869 - var(--inset-wide)); } /* @raw-ok measure: Figma 1129:6781 phone overhang below the card, 389.6/262 of the phone width - padding */
  .wg-outcomes-card__phone { position: relative; right: auto; top: auto; transform: none; align-self: center; width: var(--wg-signoff-phone-w); aspect-ratio: 262 / 569; margin-bottom: calc(-1.4869 * var(--wg-signoff-phone-w)); } /* @raw-ok measure: Figma 1129:6779 phone 262 x 569, part below the card's content box 389.6/262 */
  .wg-signoff__page { width: calc(100% + 2 * var(--layout-margin)); margin-inline: calc(-1 * var(--layout-margin)); aspect-ratio: 390 / 632; border-radius: var(--radius-compact); }

  /* --- next case study: Figma's angular gradient, 60/24 padding, content height --- */
  /* Figma Focus 12: a whole screen, content centred */
  .wg-next { min-height: 100svh; padding: var(--spacing-60) 0; background: var(--surface-accent) var(--gradient-angular-accent) 0 0 / 100% 100% no-repeat; }
  .wg-next__inner { width: calc(100% - 2 * var(--layout-margin)); }
  .wg-next__text .heading-m { font-size: var(--type-heading-l-size); } /* @raw-ok one-off: next-case title, 32 on phone (Figma): heading-l's phone size */
  .wg-next .wg-visual { flex: none; min-height: 0; aspect-ratio: 342 / 228; }
  .wg-next .wg-next__cover { align-self: stretch; } /* the hover rules' flex-start left the cover 0 x 0 in the column */
}
@keyframes wg-chip-float {
  from { translate: 0 0; }
  to   { translate: 0 -4px; }
}
@media (prefers-reduced-motion: reduce) { .chip-m { animation: none; } }
html.motion-paused .chip-m { animation-play-state: paused; }
