/* ============================================================
   AL BUTTER — main.css
   Consumes ONLY --z-* tokens. Logical properties only, so RTL is free.

   Surface rhythm follows the system: Churn is the default field,
   Crème is paper for cards/forms/long reading. Sections alternate
   between the two rather than using shadows — SHADOW is listed
   under the logo DON'Ts, so elevation is tone and border, not blur.
   ============================================================ */

/* ── numerals ─────────────────────────────────────────────
   §08: "percentages, currency and handles stay left-to-right inside
   the sentence." Digit GLYPHS follow the page (build-time, via the
   {{~}} token); this keeps the DIRECTION of a numeric token LTR so
   "98.9%" and "8,000 JD" don't get reordered inside an RTL line. */
.num {
  direction: ltr;
  unicode-bidi: isolate;
  font-variant-numeric: tabular-nums;
}

/* ── icons ───────────────────────────────────────────────── */
.icon {
  display: inline-block;
  inline-size: 1.25em;
  block-size: 1.25em;
  vertical-align: -0.25em;
  flex-shrink: 0;
}
.icon--sm { inline-size: 1em; block-size: 1em; }
/* §07: "Arrows and chevrons point inline-end — they flip with
   direction. Everything else is symmetrical, so RTL costs nothing."
   §08: "The mark never mirrors" — which is why this is opt-in per
   icon rather than a blanket rule on the sprite. */
html[dir="rtl"] .icon--dir { transform: scaleX(-1); }
.logo-mark { flex-shrink: 0; }

/* On a Crème surface the block's pale face becomes Churn, and vice
   versa, so the mark never loses its top plane. */
.card,
.site-footer,
.contact,
.formats { --icon-light: var(--z-churn); }
.hero,
.games-pointer { --icon-light: var(--z-creme); }

/* ── header ─────────────────────────────────────────────── */
.site-header {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-layer-header);
  background: var(--z-header-bg);
  border-block-end: 1px solid var(--z-hairline);
}
.site-header__inner {
  display: flex;
  align-items: center;
  gap: var(--z-space-6);
  min-block-size: var(--z-header-h);
}
.site-header__logo {
  display: inline-flex;
  align-items: center;
}
/* ── the English wordmark lockup ─────────────────────────
   The English header used to read "AL / BUTTER" stacked, which scanned
   as a two-word restaurant sign (AlBurger is a real chain here) rather
   than as one made-up name. So English now leads with the real name in
   Arabic and carries the Latin transliteration beneath it as support.
   That also finally makes the two headers agree: Arabic has always been
   a bare الزبدة, and English was the odd one out. */
.wordmark {
  /* One knob drives the whole lockup. Both lines are sized off --wm, so
     the flush left AND right edges below survive any rescale; setting the
     two sizes independently is what broke it the first time. */
  --wm: 36px;
  display: inline-flex;
  flex-direction: column;
  align-items: start;
  line-height: 1;
}
/* The Arabic is the wordmark now, so it carries the lockup's full size in
   the display face. isolate, not plain rtl: this is a lone RTL run inside
   an LTR document, and without isolation the bidi algorithm lets it
   reorder against whatever punctuation or Latin sits beside it. */
.wordmark__ar {
  display: block;
  direction: rtl;
  unicode-bidi: isolate;
  font-size: var(--wm);
  letter-spacing: 0;   /* display tracking is for Latin caps; it breaks Arabic joins */
}
/* Transliteration, not a second name: lowercase, dropped to the soft ink,
   and tracked so it sets to the same measure as the Arabic above it
   (64.7px vs 65.3px at --wm:36px, measured in DM Mono). Held at exactly
   one third of --wm, which puts it on the 12px floor from tokens.css in
   the header — the Arabic was sized UP to meet that floor rather than the
   caption sized down through it. */
.wordmark__sub {
  display: block;
  margin-block-start: calc(var(--wm) * .11);
  /* Pinned to the mono by name, NOT through --z-font-label. The Arabic
     pages remap that token to Rubik so their eyebrows stop falling back to
     a system mono, and "albutter" is Latin — it would have been swept up
     and quietly restyled in the one place on the site that must not move. */
  font-family: "DM Mono", ui-monospace, monospace;
  font-size: calc(var(--wm) / 3);
  font-weight: var(--z-weight-bold);
  letter-spacing: .08em;
  text-transform: lowercase;
  color: var(--z-ink-soft);
}
/* Footer has the room, and the reversed ground needs the lighter ink. */
.site-footer__wordmark .wordmark { --wm: 42px; }
.site-footer__wordmark .wordmark__sub { color: var(--z-ink-rev-soft); }

/* Wordmark is ALL CAPS — the only place caps are allowed. */
.site-header__name {
  /* Flex, not inline text. As an inline box the stacked lockup was
     baseline-aligned, so the body line-height's descent slack (1.65 of
     18px) sat entirely ABOVE it and dropped the wordmark 5.5px below the
     nav's centre line. line-height:1 kills the strut; align-items
     centres what is left. */
  display: inline-flex;
  align-items: center;
  line-height: 1;
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-black);
  font-size: 1.125rem;
  letter-spacing: var(--z-tracking-display);
  text-transform: uppercase;
  white-space: nowrap;   /* the wordmark never breaks across lines */
}

.site-nav {
  display: none;
  align-items: center;
  gap: var(--z-space-6);
  /* Nav sits BESIDE the logo. It must not take the auto margin — the
     actions group owns that, otherwise nav and actions travel to the far
     end together and leave a dead gap across the middle of the bar. */
  margin-inline-start: var(--z-space-8);
  font-size: var(--z-text-small);
  font-weight: var(--z-weight-medium);
  color: var(--z-fg-muted);
}
.site-nav a { white-space: nowrap; transition: color var(--z-dur-fast) var(--z-ease-out); }
.site-nav a:hover { color: var(--z-fg); }

/* Games is a destination, not an in-page anchor — chip styling and the
   PAT mark separate it from the section links beside it. */
.site-nav__tab {
  display: inline-flex;
  align-items: center;
  gap: var(--z-space-2);
  padding-block: var(--z-space-2);
  padding-inline: var(--z-space-4);
  border: 2px solid var(--z-hairline-strong);
  border-radius: 2px;
  clip-path: var(--z-chamfer);
  color: var(--z-fg);
  min-block-size: var(--z-hit-min);
}
.site-nav__tab:hover { border-color: var(--z-jam); background: var(--z-creme); }

/* Every pill in the bar is the same height. Left to their own padding they
   land on 44 / 45 / 49px, which reads as three slightly wrong buttons
   rather than one row. */
.site-nav__tab,
.lang-toggle,
.site-header__cta { block-size: var(--z-hit-min); padding-block: 0; }

.site-header__actions {
  display: flex;
  align-items: center;
  gap: var(--z-space-3);
  margin-inline-start: auto;
}
.lang-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--z-hit-min);
  min-block-size: var(--z-hit-min);
  border: 2px solid var(--z-hairline-strong);
  border-radius: 2px;
  clip-path: var(--z-chamfer);
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  font-weight: var(--z-weight-medium);
  letter-spacing: .1em;
  color: var(--z-fg-muted);
  transition: color var(--z-dur-fast) var(--z-ease-out),
              border-color var(--z-dur-fast) var(--z-ease-out);
}
/* The toggle carries lang="<the other language>", so this hits the ع on the
   English pages and never the "EN" on the Arabic ones. A single Arabic
   glyph at label size sat lost in a 44px circle; DM Mono has no Arabic
   either, so it also needs the body stack to reach Palestine. Letter
   spacing goes to zero — on one character it is pure trailing space and
   pushes the glyph off the circle's centre. */
.lang-toggle[lang="ar"] {
  font-family: var(--z-font-body);
  font-size: 1.35rem;   /* 1.6875rem − 20% */
  letter-spacing: 0;
  /* ع has a deep tail, so at this size the ink landed 5.8px low — 14.1px of
     air above it and 2.5px below, the tail almost touching the ring. The
     block-size is fixed and box-sizing is border-box, so end-padding pulls
     the content box up by half its value without changing the circle. */
  padding-block: 0 .43em;
}
.lang-toggle:hover { color: var(--z-jam); border-color: var(--z-jam); }
.site-header__cta { display: none; }

/* ── mobile menu ─────────────────────────────────────────
   Below 768px the bar has room for the logo, the language toggle and one
   control. Everything else — the section links, the games tab and the CTA
   — moves into a panel under the bar. Before this the nav was simply
   display:none on a phone, so there was no route to any section and the
   funnel's own button was invisible.

   The toggle ships `hidden` and nav.js reveals it. With JS off the panel
   is a plain stacked list that is always open, which is navigable; a
   toggle with no script behind it would not be. */
.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--z-hit-min);
  block-size: var(--z-hit-min);
  border: 2px solid var(--z-hairline-strong);
  border-radius: 2px;
    clip-path: var(--z-chamfer-sm);
  background: none;
  color: var(--z-fg);
  cursor: pointer;
  transition: border-color var(--z-dur-fast) var(--z-ease-out);
}
.nav-toggle:hover { border-color: var(--z-jam); }

/* Three bars on a fixed 22x18 field so the rotated arms meet exactly at
   the centre. Sized in px, not em, because the arms are geometry rather
   than type — an em here would drift with the label scale. */
.nav-toggle__bars {
  position: relative;
  display: block;
  inline-size: 22px;
  block-size: 18px;
}
.nav-toggle__bars i {
  position: absolute;
  inset-inline: 0;
  block-size: 2.5px;
  border-radius: var(--z-radius-full);
  background: currentColor;
  transition: transform var(--z-dur-base) var(--z-ease-in-out),
              opacity var(--z-dur-fast) linear;
}
.nav-toggle__bars i:nth-child(1) { inset-block-start: 0; }
.nav-toggle__bars i:nth-child(2) { inset-block-start: 7.75px; }
.nav-toggle__bars i:nth-child(3) { inset-block-start: 15.5px; }

/* Open: the outer bars converge on the middle and cross; the middle one
   scales out from the centre so nothing pops. */
.nav-toggle[aria-expanded="true"] .nav-toggle__bars i:nth-child(1) {
  transform: translateY(7.75px) rotate(45deg);
}
.nav-toggle[aria-expanded="true"] .nav-toggle__bars i:nth-child(2) {
  transform: scaleX(0);
  opacity: 0;
}
.nav-toggle[aria-expanded="true"] .nav-toggle__bars i:nth-child(3) {
  transform: translateY(-7.75px) rotate(-45deg);
}

@media (max-width: 767.98px) {
  /* .site-header is sticky, which makes it a containing block, so the
     panel hangs off it without any extra wrapper. */
  .site-nav {
    display: flex;
    position: absolute;
    inset-inline: 0;
    inset-block-start: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: var(--z-space-1);
    margin-inline-start: 0;
    padding-block: var(--z-space-3) var(--z-space-6);
    padding-inline: var(--z-container-pad-inline);
    background: var(--z-header-bg);
    border-block-end: 1px solid var(--z-hairline);
    font-size: var(--z-text-body);
  }
  /* :not(.btn) matters — `.site-nav a` outranks `.btn--primary` on
     specificity, so without it the CTA was painted Jam-on-Jam and its
     label vanished. The dividers skip it for the same reason: it is a
     button sitting below the list, not a row in it. */
  .site-nav a:not(.btn) {
    display: flex;
    align-items: center;
    gap: var(--z-space-2);
    min-block-size: var(--z-hit-min);
    color: var(--z-fg);
  }
  .site-nav > a:not(.btn) + a:not(.btn) {
    border-block-start: 1px solid var(--z-hairline);
  }
  .site-nav__tab { border: 0; padding-inline: 0; border-radius: 0; }
  .site-nav__cta { justify-content: center; margin-block-start: var(--z-space-3); }

  /* Closed. visibility (not display) so the panel can transition, and so
     its links leave the tab order while shut. */
  .site-nav[data-open="false"] {
    opacity: 0;
    visibility: hidden;
    transform: translateY(calc(var(--z-space-2) * -1));
    pointer-events: none;
  }
  .site-nav[data-open="true"] { opacity: 1; visibility: visible; transform: none; }
  .site-nav[data-open] {
    transition: opacity var(--z-dur-base) var(--z-ease-out),
                transform var(--z-dur-base) var(--z-ease-out),
                visibility var(--z-dur-base);
  }
}

@media (min-width: 768px) {
  /* Games tab is a destination and earns its place before the section
     links do, so it shows first. */
  .site-nav { display: flex; }
  .site-nav > a:not(.site-nav__tab) { display: none; }
  .site-header__cta { display: inline-flex; }
  .nav-toggle,
  .site-nav__cta { display: none; }
}
@media (min-width: 1024px) {
  /* :not(.site-nav__cta) is load-bearing. Without it this selector — which is
     more specific than `.site-nav__cta { display:none }` above — re-shows the
     panel's CTA on desktop, and "Start a project" renders twice in the bar. */
  .site-nav > a:not(.site-nav__tab):not(.site-nav__cta) { display: inline; }
}

/* ── shared section furniture ───────────────────────────── */
.section__title {
  font-size: var(--z-text-h1);
  letter-spacing: var(--z-tracking-h1);
  margin-block: var(--z-space-3) var(--z-space-6);
  max-inline-size: 24ch;
}
/* §07 PULL QUOTE — ONE PER PAGE. "Jam surface, Churn ink, no shadow.
   This is the only place on the page type goes above 28px besides
   the hero." */
/* ── the line · full-bleed dark band ─────────────────────
   Was a rounded Jam rectangle sitting inside the container, which is
   structurally a button: same fill, same radius family as .btn--primary.
   The fix is not a smaller button, it is not being a button. Edge to edge,
   no radius, no border — a band the page passes through rather than an
   object sitting on it.

   The 100vw pull is written as 50% - 50vw rather than a negative margin so
   it is direction-agnostic and survives RTL untouched. Inner padding keeps
   the text on the same gutter as every other section, so the type still
   lines up with the column above it even though the band does not. */
.pull {
  margin-block-start: var(--z-space-16);
  margin-inline-start: calc(50% - 50vw);
  inline-size: 100vw;
  max-inline-size: 100vw;
  padding-block: var(--z-space-16);
  padding-inline: var(--z-container-pad-inline);
  border-radius: 0;
  background: var(--z-jam);
  color: var(--z-ink-rev);
  font-family: var(--z-font-display);
  font-size: var(--z-text-h2);
  font-weight: 700;
  letter-spacing: -.01em;
  line-height: var(--z-leading-snug);
  text-align: center;
}
.pull br { display: block; }

/* Alternating surfaces carry the rhythm instead of shadows. */
.formats,
.clients,
.contact { background: var(--z-creme); }

/* ── hero ───────────────────────────────────────────────── */
.hero {
  position: relative;
  overflow: hidden;   /* the photo strip leaves over the top edge */
  padding-block: var(--z-space-16) var(--z-space-24);
  overflow: hidden;
}
.hero__inner {
  display: grid;
  gap: var(--z-space-12);
  align-items: center;
}
/* Single column. This used to be 1.15fr .85fr — copy on one side, the
   morph animation on the other. With the animation removed the halves were
   left sitting in a 53% column with the other 47% empty, which is what made
   the hero look wrong on a wide screen: two tall narrow panels hugging the
   inline edge. Nothing replaces the second column; the halves take the
   width and cap themselves. */
@media (min-width: 1024px) {
  .hero__inner { grid-template-columns: 1fr; }
}
.hero__title {
  font-size: var(--z-text-display);
  letter-spacing: var(--z-tracking-display);
  max-inline-size: 18ch;
  margin-block: var(--z-space-4);
}
/* Melt Gold as display type is only legal at 21px+ over Jam. The hero
   title is far above that but sits on Churn, so the accent line is Jam
   with a Melt Gold underline instead of gold text. */
/* Drawn as a repeating background rather than an absolutely-positioned
   ::after: the accent wraps across lines, and a single positioned bar
   spans the whole inline-block box — so it overshot the shorter last
   line. box-decoration-break makes it follow each line fragment. */
.hero__accent {
  display: inline;
  background-image: linear-gradient(var(--z-gold), var(--z-gold));
  background-repeat: no-repeat;
  background-size: 100% .14em;
  background-position: 0 var(--z-accent-offset);
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}
.hero__lede { margin-block-end: var(--z-space-8); }
.hero__actions { display: flex; flex-wrap: wrap; gap: var(--z-space-3); }
/* 10% over the site-wide .btn — every measurement scaled together
   (padding, gap, type, hit target) rather than a transform:scale, which
   would blur the text at a fractional size and inflate the invisible
   hit-box past what actually looks clickable. calc() on the existing
   tokens keeps this inside the token law instead of hardcoding new
   numbers. Hero-only: it is the one place on the page a slightly louder
   CTA earns its keep. */
.hero__actions .btn {
  gap: calc(var(--z-space-2) * 1.1);
  padding-block: calc(var(--z-space-3) * 1.1);
  padding-inline: calc(var(--z-space-8) * 1.1);
  font-size: calc(var(--z-text-small) * 1.1);
  min-block-size: calc(var(--z-hit-min) * 1.1);
}

.hero__art { display: grid; place-items: center; }
/* ── hero scroll cue ─────────────────────────────────────
   Was a left-aligned horizontal bar at --z-fg-faint (2.21:1), which is
   below any contrast floor and read as a stray rule rather than an
   invitation. Now centred under the hero, on the muted ink, and pointing
   the way it wants you to go: a dot falling down a vertical track. */
.hero__scroll {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--z-space-3);
  margin-block-start: var(--z-space-16);
  padding-inline: var(--z-container-pad-inline);
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  letter-spacing: var(--z-tracking-label);
  text-transform: uppercase;
  color: var(--z-fg-muted);
}
.hero__scroll-track {
  position: relative;
  display: block;
  inline-size: 2px;
  block-size: 3.5rem;
  border-radius: var(--z-radius-full);
  background: var(--z-hairline-strong);
  overflow: hidden;
}
.hero__scroll-dot {
  position: absolute;
  inset-inline: 0;
  inset-block-start: 0;
  block-size: 1.25rem;
  border-radius: var(--z-radius-full);
  background: var(--z-jam);
  animation: scrollHint var(--z-dur-scroll-cue) var(--z-ease-in-out) infinite;
}
@keyframes scrollHint {
  0%       { transform: translateY(-100%); opacity: 0; }
  35%, 65% { opacity: 1; }
  100%     { transform: translateY(280%); opacity: 0; }
}

/* The cue is an invitation, not information — under reduced motion the
   dot simply rests at the top of the track instead of falling. */
@media (prefers-reduced-motion: reduce) {
  .hero__scroll-dot { animation: none; }
}

}

/* ============================================================
   HERO LOOP — SMILING PAT · SPREAD · BUTTER BLOCK

   The animation itself lives in assets/js/hero-loop.js — it is a path
   morph driven frame by frame, so there are no keyframes here. This
   block is layout only, plus the reduced-motion resting state.
   ============================================================ */
.hero-loop {
  display: grid;
  justify-items: center;
  gap: var(--z-space-4);
  /* Kept modest below the two-column breakpoint, where the loop stacks
     under the copy and would otherwise push the CTAs off-screen. */
  inline-size: min(13rem, 52vw);
}
@media (min-width: 1024px) {
  .hero-loop { inline-size: min(22rem, 30vw); gap: var(--z-space-6); }
}
.hero-loop svg {
  inline-size: 100%;
  height: auto;
  aspect-ratio: 1;
  /* The knife enters and exits off-canvas and must be clipped, or it
     escapes the artwork and crosses the headline. */
  overflow: hidden;
}

/* State name, rewritten by the loop. Reserved height so swapping a
   longer word for a shorter one cannot nudge the layout. */

/* Reduced motion: hero-loop.js returns before touching anything, so the
   markup's baked-in BUTTER BLOCK path and resting face are what show.
   Nothing to override here — the static state IS the fallback. */

/* ── reach / donut ──────────────────────────────────────────
   Formerly its own "98.9%" section; that section is gone and this figure
   (plus the .pull quote below) now lives at the end of .audience instead.
   The classes kept their old names on purpose — renaming them would have
   meant touching the JS that fills the donut (reveal.js's barIO) and the
   inline {{@headline_stats...}} bindings in index.html for no real gain.
   --z-creme on .donut::after assumes a Churn-toned parent surface, which
   .audience is, same as .reach was. */
.reach__figure {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* Donut + legend sit as one centred pair rather than hugging the inline
     start. Also keeps them centred once flex-wrap drops the legend under
     the donut on narrow screens. */
  justify-content: center;
  gap: var(--z-space-8);
  margin-block-start: var(--z-space-12);
}
.donut {
  --pct: 0;
  position: relative;
  inline-size: clamp(9rem, 22vw, 12rem);
  aspect-ratio: 1;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: conic-gradient(var(--z-gold) calc(var(--fill, 0) * 1%), var(--z-chart-track) 0);
  transition: --fill var(--z-dur-slow) var(--z-ease-out);
}
.donut::after {
  content: "";
  position: absolute;
  inset: 15%;
  border-radius: 50%;
  background: var(--z-creme);
}
.donut.is-filled { --fill: var(--pct); }
.donut__value {
  position: relative;
  z-index: var(--z-layer-raised);
  font-family: var(--z-font-display);
  font-size: var(--z-text-h2);
  font-weight: var(--z-weight-black);
  letter-spacing: var(--z-tracking-h1);
}
.reach__legend { color: var(--z-fg-muted); }
.reach__legend strong {
  color: var(--z-fg);
  font-family: var(--z-font-display);
  font-size: var(--z-text-h2);
}
.reach__legend p + p { margin-block-start: var(--z-space-3); }

/* Animating a custom property needs @property to interpolate. */
@property --fill {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

/* ── audience ───────────────────────────────────────────── */
.audience__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--z-space-6);
  margin-block-start: var(--z-space-12);
}
.audience__panel-title {
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  font-weight: var(--z-weight-medium);
  letter-spacing: var(--z-tracking-label);
  text-transform: uppercase;
  color: var(--z-fg-muted);
  margin-block-end: var(--z-space-6);
}
.split-bar {
  display: flex;
  block-size: .875rem;
  border-radius: var(--z-radius-full);
  overflow: hidden;
  background: var(--z-chart-track);
}
.split-bar__seg {
  inline-size: 0;
  transition: inline-size var(--z-dur-slow) var(--z-ease-out);
}
.split-bar.is-filled .split-bar__seg { inline-size: calc(var(--pct) * 1%); }
/* §07: "Charts use exactly two fills: Gold for data, Jam at 10% for
   the track. The leading value goes Jam so the eye lands there first.
   No third color, no legend." */
.split-bar__seg--a { background: var(--z-jam); }    /* leading value */
.split-bar__seg--b { background: var(--z-gold); }

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--z-space-4);
  list-style: none;
  margin-block-start: var(--z-space-4);
  font-size: var(--z-text-small);
  color: var(--z-fg-muted);
}
.legend b { color: var(--z-fg); margin-inline-start: var(--z-space-1); }
.legend__dot {
  display: inline-block;
  inline-size: .625rem;
  block-size: .625rem;
  border-radius: 50%;
  margin-inline-end: var(--z-space-2);
}
.legend__dot--a { background: var(--z-jam); }
.legend__dot--b { background: var(--z-gold); }

.bars { list-style: none; }
.bars__row {
  display: grid;
  grid-template-columns: 4.5rem 1fr 3rem;
  align-items: center;
  gap: var(--z-space-3);
  font-size: var(--z-text-small);
}
.bars__row + .bars__row { margin-block-start: var(--z-space-3); }
.bars__label { color: var(--z-fg-muted); }
.bars__track {
  block-size: .625rem;
  border-radius: var(--z-radius-full);
  background: var(--z-chart-track);
  overflow: hidden;
}
.bars__fill {
  display: block;
  block-size: 100%;
  inline-size: 0;
  border-radius: var(--z-radius-full);
  background: var(--z-gold);
  transition: inline-size var(--z-dur-slow) var(--z-ease-out);
}
/* Percentages are of the whole audience, so the widest row (~50%) would
   fill only half the track. Scaled to the max for readability — the
   number beside each bar carries the real value. */
.bars.is-filled .bars__fill { inline-size: calc(var(--pct) * 2%); }
/* The single highest row goes Jam — the eye lands on the leading
   value before it reads any of the others. */
.bars__row.is-lead .bars__fill { background: var(--z-jam); }
.bars__row.is-lead .bars__label,
.bars__row.is-lead .bars__value { font-weight: var(--z-weight-bold); }
.bars__value {
  text-align: end;
  font-variant-numeric: tabular-nums;
  font-weight: var(--z-weight-medium);
}
.audience__highlight {
  margin-block-start: var(--z-space-6);
  padding-block-start: var(--z-space-4);
  border-block-start: 1px solid var(--z-hairline);
  font-size: var(--z-text-small);
  color: var(--z-fg-muted);
}
.audience__highlight b {
  font-family: var(--z-font-display);
  font-size: var(--z-text-h2);
  color: var(--z-fg);
}

/* ── age panel: headline cut + disclosure ────────────────
   The 18+ figure is the panel's lead, so it sits ABOVE the bars and is
   sized like a stat rather than a caption — .audience__highlight is the
   opposite arrangement (a footnote under the data) and is still used by
   other panels, so this is a sibling rule, not an override. */
.audience__lead {
  margin-block: var(--z-space-4) 0;
  font-size: var(--z-text-small);
  color: var(--z-fg-muted);
}
.audience__lead b {
  display: block;
  font-family: var(--z-font-display);
  font-size: var(--z-text-h1);
  line-height: 1;
  color: var(--z-fg);
}

.audience__more {
  margin-block-start: var(--z-space-6);
  padding-block-start: var(--z-space-4);
  border-block-start: 1px solid var(--z-hairline);
}
/* list-style on the summary itself kills the disclosure triangle in
   Firefox; ::-webkit-details-marker is the Safari half. Both are needed —
   neither alone clears it in every engine. */
.audience__more-toggle {
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--z-space-2);
  cursor: pointer;
  font-size: var(--z-text-small);
  font-weight: 600;
  color: var(--z-fg-muted);
}
.audience__more-toggle::-webkit-details-marker { display: none; }
.audience__more-toggle:hover { color: var(--z-fg); }
.audience__more-toggle:focus-visible {
  outline: 2px solid var(--z-focus-ring);
  outline-offset: 3px;
}
/* Chevron drawn in CSS rather than borrowed from the sprite: the sprite's
   #u-chevron is Jam ink at a fixed stroke, and this needs to follow the
   summary's own muted colour through hover. Rotates to point down when
   open, and is mirrored under RTL by the logical-property transform. */
.audience__more-toggle::after {
  content: "";
  inline-size: .5em;
  block-size: .5em;
  border-inline-end: 2px solid currentColor;
  border-block-end: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform var(--z-dur-fast, 160ms) ease;
}
[dir="rtl"] .audience__more-toggle::after { transform: rotate(-135deg); }
.audience__more[open] .audience__more-toggle::after,
[dir="rtl"] .audience__more[open] .audience__more-toggle::after {
  transform: rotate(45deg);
}
.audience__more .bars { margin-block-start: var(--z-space-4); }

@media (prefers-reduced-motion: reduce) {
  .audience__more-toggle::after { transition: none; }
}

/* ── V2 · audience lead, full_chart option ───────────────
   The markup always ships the eighteen_plus arrangement, because that is
   the default and the no-JS render has to be the default. This strips it
   back to a plain chart: the headline figure goes, and the disclosure
   loses its chrome so the bars read as the panel's own content rather
   than as something that was revealed. variants.js opens the <details>;
   CSS cannot. Nothing here changes a number — same six bands either way. */
[data-v-audience="full_chart"] .audience__lead { display: none; }
[data-v-audience="full_chart"] .audience__more-toggle { display: none; }
[data-v-audience="full_chart"] .audience__more {
  margin-block-start: 0;
  padding-block-start: 0;
  border-block-start: 0;
}
[data-v-audience="full_chart"] .audience__more .bars { margin-block-start: var(--z-space-4); }

/* The metric window label — hidden until a window is supplied in config.
   Inline and quiet: it qualifies the figure, it is not a second figure. */
.v-window { color: var(--z-fg-faint); }
.v-window:not([hidden])::before { content: " ("; }
.v-window:not([hidden])::after  { content: ")"; }

/* ── hero: the reach figure leads ────────────────────────
   Sits where the eyebrow used to, so the hero's vertical rhythm is
   unchanged, but it is a STAT, not a label — the number takes display
   weight and the sentence stays small beside it. Baseline-aligned rather
   than stacked so the two read as one line of speech at desktop width,
   and wraps to two lines on a phone without the number shrinking. */
.hero__proof {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--z-space-2) var(--z-space-3);
  margin-block-end: var(--z-space-4);
  max-inline-size: 34ch;
}
.hero__proof b {
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-black);
  font-size: var(--z-text-stat);
  line-height: 1;
  letter-spacing: var(--z-tracking-h1);
  color: var(--z-jam);
}
.hero__proof span {
  font-size: var(--z-text-small);
  line-height: 1.45;
  color: var(--z-fg-muted);
  flex: 1 1 12ch;
}

/* ── ethics / terms of engagement ────────────────────────
   Two columns of paired statements. Reads as a document rather than as
   marketing on purpose: no cards, no icons, no accent fills — a rule and
   a heading, which is the visual language a compliance reader trusts.
   The one place it borrows from the brand is the Gold rule on the
   "won't" item, because that is the answer nobody expects to find. */
/* ── how I work ───────────────────────────────────────────
   Four independent notes. Kept as plain as it is possible to be: heading,
   then its paragraph, stacked, one after another. No columns, no rules, no
   bullets, no numerals, nothing that pairs them or ranks them.

   Everything fancier that was tried here made it worse. A two column grid
   implied opposition. A run-in gold dot read as a bullet. Numerals hung in
   a margin column broke outright — ::before took column one and the h3
   took column two, so the paragraph wrapped onto the next row INTO the
   2.75rem numeral column and set one word per line.

   Spacing is the only device now: a big gap between points, a small one
   inside each, and a measure short enough to read. That is enough. */
.ethics__list {
  display: block;
  list-style: none;
  margin-block-start: var(--z-space-12);
  padding: 0;
  max-inline-size: 44rem;
}
.ethics__list > li {
  border: 0;
  padding: 0;
  margin-block-end: var(--z-space-8);
}
.ethics__list > li:last-child { margin-block-end: 0; }
.ethics__list h3 {
  font-size: var(--z-text-body);
  font-weight: 700;
  color: var(--z-jam);
  margin: 0 0 var(--z-space-2);
}
.ethics__list p {
  font-size: var(--z-text-small);
  line-height: 1.75;
  color: var(--z-fg-muted);
  margin: 0;
  text-wrap: pretty;
}

.ethics__note {
  margin-block-start: var(--z-space-12);
  padding-block-start: var(--z-space-6);
  border-block-start: 1px solid var(--z-hairline);
  font-size: var(--z-text-small);
  color: var(--z-fg-muted);
  max-inline-size: 62ch;
}

/* ── top reel · the 18.4M scene ──────────────────────────
   The number and the video share one composition instead of the number
   living in a stat box. Video first in the source order so a screen reader
   and a no-CSS render both reach the thing before its caption.

   The figure is capped rather than fluid: an Instagram embed is a fixed
   cross-origin document, and letting it grow to a desktop column makes it
   letterbox badly. 380px is about where the reel fills the frame. */
.topreel {
  display: grid;
  gap: var(--z-space-8);
  align-items: center;
  margin-block-start: var(--z-space-12);
}
@media (min-width: 48rem) {
  .topreel { grid-template-columns: minmax(0, 380px) 1fr; }
}
.topreel__figure { margin-block-end: 0; }

/* The number is the largest thing in the section and carries no container,
   so it reads as a statement rather than a metric in a tile. */
.topreel__views {
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-black);
  font-size: clamp(3rem, 1.5rem + 8vw, 6.5rem);
  line-height: .9;
  letter-spacing: var(--z-tracking-display);
  color: var(--z-jam);
  margin: 0;
}
.topreel__label {
  font-size: var(--z-text-body);
  color: var(--z-fg-muted);
  margin: var(--z-space-3) 0 0;
  max-inline-size: 20ch;
}
.topreel__link {
  display: inline-block;
  margin-block-start: var(--z-space-6);
  font-size: var(--z-text-small);
  font-weight: 700;
  color: var(--z-jam);
  text-underline-offset: 4px;
}

/* ── FULL BLEED · the top reel scene ─────────────────────
   Applies to a self-hosted file AND to a cropped embed. The earlier note
   here said an embed could never be full bleed — that was true of styling
   the chrome away, and wrong about clipping it. The crop above solves it,
   so the scene no longer waits on a file.

   The band goes edge to edge and dark; the reel keeps its own portrait
   shape inside it rather than being scaled up to fill a wide viewport.
   Blowing a 1080-wide vertical video up to 1900px to avoid letterboxing
   trades the letterbox for visible softness, which is a worse deal.

   Breaks the container with a 100vw pull rather than a negative margin
   hack, so it survives RTL: 50% minus 50vw is direction-agnostic. */
.topreel.is-bleed {
  grid-template-columns: 1fr;
  gap: 0;
  inline-size: 100vw;
  max-inline-size: 100vw;
  margin-inline-start: calc(50% - 50vw);
  margin-block-start: var(--z-space-16);
  position: relative;
}
/* Dark ground so the reel sits in a band rather than on cream, and the
   portrait frame is centred in it with a capped height. 78vh keeps the
   whole reel on screen at once on a laptop without pushing the number off. */
.topreel.is-bleed {
  /* Was #1a1a1a. A black band in a buttery page reads as a hole cut in it,
     and worse, the reel's own dark slot background showed as a hard edge
     wherever the cropped embed did not land pixel-exact. On the churn the
     band is still a distinct field — it is the surface token, a step off
     the creme the section sits on — but nothing has an edge to misalign
     against. */
  background: var(--z-churn);
  padding-block: var(--z-space-8);
  padding-inline: var(--z-space-6);
  /* Reel and figures side by side. A 9:16 reel in a full-width band leaves
     space either side no matter what, so the only real question is what
     occupies it — and unoccupied it read as a hole in the page. The
     engagement numbers moved up out of their own card below to fill it,
     which also removes a whole block from the section. */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  /* space-12, not space-10 — there is no space-10 on the scale (1 2 3 4 6
     8 12 16 24), so this declaration was invalid and being dropped, and the
     gap was 0: the reel sat flush against the figures and clipped the end
     of the headline number. */
  gap: var(--z-space-12);
  position: relative;
  overflow: hidden;
}
/* The same figure again, enormous and almost invisible, carrying the rest
   of the band. No new content: it is the number already being quoted. */
.topreel__wash {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: grid;
  align-content: center;
  gap: .08em;
  pointer-events: none;
  overflow: hidden;
}
.topreel__wash span {
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-black);
  font-size: clamp(3rem, 9vw, 7rem);
  line-height: .92;
  white-space: nowrap;
  text-align: center;
  /* Stepped down from .07/.16. This is texture behind a column of 12px
     labels and 22px numbers; at the old weight its strokes were reading as
     part of them, which is most of why the figures were hard to pick out. */
  color: rgb(92 14 44 / .045);
}
.topreel__wash span:nth-child(even) { color: rgb(201 154 34 / .10); }
/* B3 · filled, gold rule. The frame was 430x660 — ratio 0.65, taller than
   the 4:5 Instagram actually renders, which is what let the likes row back
   into shot underneath it and pushed the band to 736px. Now a true 9:16
   filled by the crop above, with the number and caption set on one line
   under a gold rule the width of the reel. */
.topreel.is-bleed .topreel__figure {
  border-radius: 0;
  /* 330, not 250. Two reasons, and the second one is the bug.

     The band is ~870 wide and held a 250px reel next to a 336px column,
     which left most of it empty — the thing this bleed treatment exists to
     avoid in the first place.

     And the crop scales the iframe to 158% of the slot, so a 250px slot
     asked Instagram for a 395px-wide embed. Below roughly 400px their
     embed stacks its header into a taller block — about 176px of chrome
     above the video instead of the ~58px it uses at the deck's 553px — and
     the centring maths, which assumes the wider layout, put the video 93px
     down its own window. That is the black band across the top of the
     reel. At 330 the embed is 521px wide and lands in the same layout as
     every other reel on the page. */
  inline-size: min(100%, 330px);
  max-inline-size: none;
  block-size: auto;
  aspect-ratio: 9 / 16;
  position: relative;
  z-index: 1;                     /* above the wash */
}
.topreel.is-bleed .topreel__figure video { object-fit: cover; }

.topreel.is-bleed .topreel__meta {
  position: relative;
  z-index: 1;
  inline-size: min(100%, 21rem);
  display: grid;
  justify-items: start;
  gap: var(--z-space-4);
  text-align: start;
}
/* The gold rule rides the caption's top border rather than a pseudo-element
   on the meta: as a ::before it is forced to be the FIRST child, which put
   the rule above the number instead of under it. */
.topreel.is-bleed .topreel__label {
  inline-size: 100%;
  border-block-start: 2px solid var(--z-gold);
  padding-block-start: var(--z-space-3);
}
/* ── THE FIGURES: a list, not a 2x2 of loose pairs ────────
   These were four cells in a two-column grid with the label stacked over
   the number. Nothing lined up: the eye had to find each pair, decide
   which of the two lines was the label, and do it four times — and it was
   doing that on top of the giant wash type, which put low-contrast strokes
   directly behind 12px labels.

   One row per figure now, label at the start, number at the end, a
   hairline under each. Every number sits on the same edge, so the four are
   comparable at a glance instead of being read one at a time — which is
   the entire job of a stats block. */
.topreel.is-bleed .topreel__stats {
  inline-size: 100%;
  margin: 0;
  display: grid;
  gap: 0;
}
.topreel.is-bleed .topreel__stats > div {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--z-space-4);
  padding-block: var(--z-space-2);
  border-block-end: 1px solid rgb(92 14 44 / .14);
}
.topreel.is-bleed .topreel__stats > div:last-child { border-block-end: 0; }
.topreel.is-bleed .topreel__meta dt {
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  letter-spacing: var(--z-tracking-label);
  /* No uppercase. It does nothing to Arabic and the English twin reads
     better in sentence case at this size next to a heavy number. */
  color: var(--z-ink-soft);
}
.topreel.is-bleed .topreel__meta dd {
  margin: 0;
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-black);
  font-size: 1.375rem;
  line-height: 1;
  color: var(--z-jam);
  /* Tabular figures so the four numbers align on their digits down the
     column rather than drifting by glyph width. */
  font-variant-numeric: tabular-nums;
}
/* THE FIGURES NEED A FACE WITH WEIGHTS. These ask for --z-weight-black,
   and in Arabic they were silently getting 400.

   --z-font-display resolves to Palestine first, and Palestine covers
   U+0600-06FF — which includes the Arabic-Indic digits ٠-٩. It ships as a
   single Regular cut with synthesis disabled on purpose (faux-bolding
   Arabic merges the connected strokes), so `font-weight: 900` on it is not
   ignored gracefully, it is simply not available: the number rendered at
   the same weight as the label beside it, at 22px, on a tinted ground.

   Rubik carries the same Arabic range at 300-900 variable, so the figures
   move to it and get a real 700. The headline number above stays on
   Palestine — at 58px a single weight reads perfectly well, and that one
   is the brand's voice rather than a data point. */
html[lang="ar"] .topreel.is-bleed .topreel__stats dd {
  font-family: "Rubik", system-ui, sans-serif;
  font-weight: var(--z-weight-bold);
}
/* Reversed out on the dark band, and the number steps down: at the old
   clamp it was 6.5rem, which fights the reel instead of captioning it. */
.topreel.is-bleed .topreel__views {
  color: var(--z-jam);
  font-size: clamp(2.5rem, 1.5rem + 4vw, 3.75rem);
}
.topreel.is-bleed .topreel__label { color: var(--z-ink-soft); margin: 0; max-inline-size: none; }
.topreel.is-bleed .topreel__link  { color: var(--z-jam); margin: 0; }

/* The number used to sit ON the video under a gradient scrim. B3 takes it
   off the footage and puts it under a gold rule instead, so nothing is
   read through moving video and nothing overlaps. The overlay rules that
   were here are gone rather than overridden — they were later in the file
   and were quietly beating the B3 block above on source order. */

@media (max-width: 47.99rem) {
  /* Phone keeps the same 9:16 the desktop band uses now; the filled crop
     works at any width, so there is nothing left for a separate portrait
     case to fix. The meta just has less room beside itself. */
  .topreel.is-bleed { gap: var(--z-space-6); }
  .topreel.is-bleed .topreel__stats > div { padding-block: var(--z-space-1); }
}

/* SUPERSEDED, kept only for slots that never reach the embed state. The
   note here used to say an embed could not be cropped to 9:16 without
   cutting Instagram's chrome off. That is no longer true: the filled crop
   in the embed rule lines the window up with the video rather than the
   frame, so 9:16 costs neither chrome nor pillars. The top reel sets its
   own 9:16 and overrides this. */
.mslot--tall { aspect-ratio: 1 / 1.62; }
/* DEFERRED — a reel is mapped but nothing is downloaded yet. Reads as
   "there is a video here, press to watch" rather than as an empty box:
   solid ground, no hatching, a real play button. The hatched treatment is
   reserved for slots with genuinely nothing behind them. */
.mslot[data-media-state="deferred"] {
  background: var(--z-creme);
  border: 1px solid var(--z-hairline-strong);
  cursor: pointer;
}
.mslot[data-media-state="deferred"] .mslot__play {
  position: static;
  inline-size: 3rem;
  block-size: 3rem;
}

/* ── EMBED · chrome cropped out ───────────────────────────
   The iframe's contents cannot be STYLED from here — it is a cross-origin
   document. But it can be CLIPPED, and that is enough: Instagram's reel
   embed puts a fixed-height account header above the video and a caption /
   likes / comment block below it. Make the iframe taller than its box by
   exactly those two amounts, pull it up by the header height, and hide the
   overflow. What is left inside the frame is the video and nothing else.

   The two offsets are tokens because they are the fragile part — Instagram
   changes that chrome without warning, and when it does, these are the only
   two numbers to re-measure. If a strip of white or a sliver of avatar
   appears at an edge, one of these is stale.

   Interaction still works inside the visible area: this clips paint, it
   does not disable the frame. */
.mslot[data-media-state="embed"] {
  --ig-chrome-top: 58px;
  --ig-chrome-bottom: 168px;
  /* Matches whatever the slot sits on rather than a fixed dark, so the
     sliver left when the crop does not land exactly reads as nothing at
     all instead of as a black rim around the video. */
  background: var(--z-churn);
  border: 0;
  /* 9:16, and the crop below is what earns it. Instagram renders a reel at
     feed aspect (4:5 at most) and pillarboxes the leftover in black, so a
     4:5 window showed the whole frame: no chrome, but black bars either
     side. Lining the window up with the VIDEO instead of with the frame
     gets both — see the iframe rule. Matching the poster box's 9:16 also
     stops the card changing shape at the moment it loads. */
  aspect-ratio: 9 / 16;
  overflow: hidden;
  position: relative;
}
/* THE FILLED CROP. Make the iframe wider than its box and pull it back by
   half the difference, so the box frames the video and Instagram's black
   pillars fall outside it entirely. 158%, not the 142.23% the geometry
   alone predicts: the theoretical figure assumes the media area is exactly
   the iframe's width and exactly 4:5, and in practice the embed carries its
   own inset — 142.23% left a thin black edge each side, measured. So this
   is deliberately over-cropped, shaving ~1-5% off each side of the video.
   Losing a sliver of a portrait talking head beats a black bar.

   The offset is symmetric, so it holds in RTL without a second rule: the
   video is centred in the iframe either way. Assumes a 9:16 source, which
   every reel here is. A natively 4:5 post would lose ~15% a side. */
.mslot[data-media-state="embed"] iframe {
  position: absolute;
  /* --ig-zoom is the iframe's width as a percentage of the slot, as a bare
     NUMBER so the offset that centres it can be derived rather than
     restated. 158 is the default and is calibrated for a 9:16 reel, which
     Instagram renders pillarboxed inside a 4:5 frame: at 158 the black
     pillars fall outside the window and the video content lands at about
     111% of it — a light horizontal crop.

     A post that is natively 4:5 has no pillars, so the same 158 is pure
     over-zoom and crops a quarter of the picture away for nothing. Those
     get their own value from variants.config.js. */
  inline-size: calc(var(--ig-zoom, 158) * 1%);
  inset-inline-start: calc((100% - var(--ig-zoom, 158) * 1%) / 2);
  /* --ig-top is measured per slot by reels.js so the video's midpoint lands
     on the frame's midpoint; the chrome-height fallback only applies for
     the instant before that runs. */
  inset-block-start: var(--ig-top, calc(-1 * var(--ig-chrome-top)));
  /* Tall enough for the whole video area plus both chrome bands, so the
     iframe never clips the video before the frame does. 158% wide at
     Instagram's 4:5 feed aspect makes the video 197.5% of the frame's
     WIDTH — expressed here against width via aspect-ratio, which is the
     one property that reads the other axis. */
  block-size: auto;
  aspect-ratio: 158 / 226;
  min-block-size: calc(100% + var(--ig-chrome-top) + var(--ig-chrome-bottom));
  border: 0;
  display: block;
}

/* The card the rotation has picked. Kept deliberately quiet: it is a hint
   that this one is live, not a second focus ring competing with the real
   one. Only the deferred state gets it — once the embed loads, the moving
   video is its own signal. */
/* Tightened, now that the row carries itself. Measured before: a 320x771
   card whose reel was 480 of that — 62%. The padding, the icon gap and the
   slot gap were each sized for a card sitting still under a scroll-lock;
   in a rotating row they just push the video down. The reel keeps its 9:16
   exactly, so the phone proportion is untouched — only the dead space
   around it moves. */
.card.format {
  padding: var(--z-space-4);
}
.card.format .format__icon {
  inline-size: 2rem;
  block-size: 2rem;
  margin-block-end: var(--z-space-3);
}
.card.format .mslot { margin-block-end: var(--z-space-3); }
.card.format h3 { margin-block-end: var(--z-space-1); }

/* The same caption plate as the top reel, on every format card: a gold
   rule between the reel and its title, so the two treatments read as one
   decision rather than the hero having its own look. */
.card.format > h3 {
  border-block-start: 2px solid var(--z-gold);
  padding-block-start: var(--z-space-3);
}

/* Same rule as the eyebrows, for the other Arabic label surfaces. The
   badge reads جديد; tracked and uppercased it was being pulled apart for
   no gain, since uppercase does nothing to Arabic in the first place.
   .lang-toggle keeps its tracking: it says EN, which is Latin. */
html[lang="ar"] .game-card__badge,
html[lang="ar"] .approaches__title,
html[lang="ar"] .site-footer__title,
html[lang="ar"] .site-footer__tagline {
  letter-spacing: 0;
  text-transform: none;
}

/* ── HERO · the two doors ─────────────────────────────────
   The site does two unrelated things and the hero has to say so in the
   first screen. Buttons could not: a primary and a secondary read as one
   action and a lesser one, not as two destinations, and both of the old
   ones pointed into the same page anyway.

   The separation is carried by GROUND, not by a rule or a gap: each door
   sits on its own field, so the split is legible before the labels are
   read. Both fields are existing tokens in new roles — the palette does
   not grow, it just stops being uniform. */
/* The header call to action sits inches from the wordmark, so it takes the
   same face. In the body font it read as a UI control that happened to be
   next to the logo; in the display face the two belong to each other.
   Scoped to the header and nav copies only — the rest of the site's buttons
   are controls inside content and stay in the reading face. */
.site-header__cta,
.site-nav__cta {
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-black);
}

/* ── HERO · two halves ────────────────────────────────────
   The site does two unrelated things, so the first screen is split down
   the middle and each half gets its own ground. The separation is carried
   by FIELD, not by a rule or a gap: two panels on the same cream would
   need reading before they read as separate, and the whole point is that
   the choice is visible first.

   It stays two halves on a phone. A stacked pair reads as one thing
   followed by another; side by side reads as a choice. */
.hero__halves {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--z-space-4);
  align-items: stretch;
  position: relative;
  z-index: 1;                       /* over the photo strip */
  /* Capped and centred. Left to fill a 1600px viewport each half becomes a
     letterbox no one can read across; at 62rem they hold roughly 3:2, which
     is the proportion the cards keep everywhere else on the site. */
  max-inline-size: 62rem;
  margin-inline: auto;
  inline-size: 100%;
}
.hero__half {
  display: flex;
  flex-direction: column;
  gap: var(--z-space-3);
  padding: var(--z-space-8) var(--z-space-6);
  border-radius: 2px;
  clip-path: var(--z-chamfer);
  min-block-size: 22rem;
}
.hero__half--work { background: var(--z-jam);  color: var(--z-creme); }
.hero__half--game { background: var(--z-gold); color: var(--z-jam);
                    justify-content: space-between; }

/* THE TWO TITLES. These name the only two things the site does, so they
   are set as titles rather than as the small tracked labels they were —
   in the display face, at heading size, in full ink. A label that has to
   be hunted for is not doing the one job it has. */
.hero__half-tag {
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-black);
  font-size: clamp(1.05rem, .82rem + .9vw, 1.4rem);
  letter-spacing: 0;
  line-height: 1.15;
  margin: 0;
}
.hero__half--work .hero__half-tag { color: var(--z-gold); }
.hero__half--game .hero__half-tag { color: var(--z-jam); }

/* The headline sits inside its half now, so it can no longer be sized for
   a full-width hero — it is in a column. */
.hero__half .hero__title {
  font-size: clamp(1.35rem, .9rem + 2.2vw, 2.75rem);
  line-height: 1.12;
  margin: 0;
  text-wrap: balance;
}
.hero__half--work .eyebrow { color: rgb(253 243 211 / .66); margin: 0; }
.hero__half--work .hero__lede { color: rgb(253 243 211 / .82); margin: 0; }

.hero__half-go {
  margin-block-start: auto;         /* both halves end on their action */
  align-self: start;
  display: inline-flex;
  align-items: center;
  gap: .5em;
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  letter-spacing: var(--z-tracking-label);
  text-transform: uppercase;
  font-weight: 700;
  text-decoration: none;
  border: 0;
  background: none;
  cursor: pointer;
  padding: var(--z-space-2) 0;
}
.hero__half--work .hero__half-go { color: var(--z-gold); }
.hero__half--game .hero__half-go { color: var(--z-jam); }
/* No mirroring here. The Arabic markup already carries ← and the English
   →, so flipping the Arabic one turned it back around to point out of
   the reading direction. Each page ships the glyph it needs. */
html[lang="ar"] .hero__half-go span { display: inline-block; }
.hero__half-go:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}
@media (prefers-reduced-motion: no-preference) {
  .hero__half-go span { transition: transform var(--z-dur-fast) var(--z-ease-out); }
  .hero__half-go:hover span { transform: translateX(4px); }
  html[lang="ar"] .hero__half-go:hover span { transform: translateX(-4px); }
}

/* ── the one playable round ───────────────────────────────
   Framed like the game frames a topic, not like the site frames a card:
   a bordered window with the term large and the other language under it.
   It is a window into the other product and should read as one.

   It is never empty. Before a round it holds the play control; during one
   the term and a live clock; after, the term it dealt, dimmed. An empty
   panel was what made the spent state look broken. */
.trial__window {
  flex: 1;
  min-block-size: 9.5rem;
  display: grid;
  place-content: center;
  gap: var(--z-space-1);
  padding: var(--z-space-4);
  text-align: center;
  background: var(--z-creme);
  border: 1.5px solid var(--z-hairline-strong);
  border-radius: 2px;
  clip-path: var(--z-chamfer);
}
.trial__window.is-spent { opacity: .62; }
/* Mid spin: the decoys are motion, not information. Softened and slightly
   lifted so the eye reads a reel travelling rather than words flashing,
   and so nobody tries to read a term that is about to be replaced. */
.trial__window.is-spinning .trial__topic { opacity: .5; filter: blur(.6px); }
.trial__window.is-spinning .trial__alt   { opacity: .25; }
/* The landing. One short pulse of the gold, which is what the game does
   when its reel stops. */
.trial__window.is-landed { border-color: var(--z-gold); }
@media (prefers-reduced-motion: no-preference) {
  .trial__window { transition: border-color .25s ease; }
  .trial__window.is-landed .trial__topic { animation: trialLand .42s var(--z-ease-out); }
  @keyframes trialLand {
    0%   { transform: scale(.94); opacity: .4; }
    60%  { transform: scale(1.03); opacity: 1; }
    100% { transform: scale(1); opacity: 1; }
  }
}
.trial__topic {
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-black);
  font-size: clamp(1.15rem, .8rem + 1.5vw, 1.9rem);
  line-height: 1.12;
  margin: 0;
  color: var(--z-jam);
  text-wrap: balance;
}
.trial__alt {
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  letter-spacing: .04em;
  margin: 0;
  color: var(--z-ink-soft);
}
/* THE CLOCK, ON ITS OWN ROW. Inside the term card it read as a third line
   of the diagnosis; out here, labelled and divided off, it reads as the
   rule of the game. Thirty seconds is the whole premise, so it should be
   stated rather than tucked under a heading. */
.trial__timer {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--z-space-3);
  margin-block-start: var(--z-space-3);
  padding-block-start: var(--z-space-3);
  border-block-start: 1px solid rgb(92 14 44 / .22);
}
.trial__timer[hidden] { display: none; }
.trial__timer-label {
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  letter-spacing: var(--z-tracking-label);
  text-transform: uppercase;
  color: rgb(92 14 44 / .7);
}
.trial__timer-value {
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-black);
  font-size: clamp(1.6rem, 1.1rem + 1.8vw, 2.4rem);
  line-height: 1;
  color: var(--z-jam);
  font-variant-numeric: tabular-nums;   /* the clock must not wobble */
}
.trial__timer.is-running .trial__timer-value { color: var(--z-jam); }
.trial__timer .trial__timer-value.is-low { color: #8B1E1E; }

.trial__hint {
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  letter-spacing: var(--z-tracking-label);
  text-transform: uppercase;
  color: rgb(92 14 44 / .78);
  margin: 0;
}
/* THE WINDOW IS THE CONTROL. It ships holding a term and reading 30, so
   the offer is legible without reading a label — it looks like a round
   waiting to happen. Idle is dimmed just enough to say "not yet running"
   without looking disabled. */
.trial__window.is-ready {
  cursor: pointer;
}
.trial__window.is-ready .trial__topic,
.trial__window.is-ready .trial__alt { opacity: .5; }
.trial__window.is-ready .trial__clock { opacity: .72; }
@media (prefers-reduced-motion: no-preference) {
  .trial__window.is-ready {
    transition: transform var(--z-dur-fast) var(--z-ease-out),
                border-color var(--z-dur-fast) var(--z-ease-out);
  }
  .trial__window.is-ready:hover {
    transform: translateY(-2px);
    border-color: var(--z-jam);
  }
  .trial__window.is-ready:hover .trial__topic,
  .trial__window.is-ready:hover .trial__alt { opacity: .72; }
}
.trial__window[role="button"]:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 3px var(--z-focus-ring);
}

/* ── the photo strip, in the hero ─────────────────────────
   Behind the halves and clipped to the section, so the frames pass BEHIND
   the two panels on their way up rather than over the reading. */
.floatstrip--hero {
  position: absolute;
  inset: 0;
  z-index: 0;
  margin: 0;
  pointer-events: none;
}
.floatstrip--hero .floatstrip__field { block-size: 100%; }
.floatstrip--hero .floatstrip__ph { opacity: .35; }

@media (max-width: 47.99rem) {
  /* space-6. This said space-5, which is not on the scale, and an invalid
     first value drops the WHOLE padding declaration — so the halves kept
     their desktop padding on a phone. */
  .hero__half { padding: var(--z-space-6) var(--z-space-4); }
  .hero__halves { gap: var(--z-space-3); }
}

/* ── PHOTO STRIP · frames rising past the reader ──────────
   Absolutely positioned inside a fixed-height field so the drift cannot
   reflow anything: only transform and opacity change, both compositor
   properties, so this costs no layout while it runs. The field keeps its
   height whether or not the photos are there, which is why the strip is
   removed outright by float.js rather than just emptied when the folder is
   empty — an empty band of dead space is worse than no band. */
.floatstrip {
  margin-block-start: var(--z-space-12);
  overflow: hidden;          /* frames leave over the top edge, not the page */
}
.floatstrip[hidden] { display: none; }
.floatstrip__field {
  position: relative;
  block-size: clamp(22rem, 46vh, 32rem);
}
.floatstrip__ph {
  position: absolute;
  inset-block-start: 0;
  margin: 0;
  inline-size: clamp(5.5rem, 13vw, 9.5rem);
  aspect-ratio: 4 / 5;
  border-radius: 2px;
  clip-path: var(--z-chamfer);
  overflow: hidden;
  background: var(--z-churn);
  will-change: transform, opacity;
}
.floatstrip__ph img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
}
/* Reduced motion: float.js leaves the transforms alone, so lay the frames
   out as a plain readable row instead of stacking them at one height. */
@media (prefers-reduced-motion: reduce) {
  .floatstrip__field {
    position: static;
    block-size: auto;
    display: flex;
    flex-wrap: wrap;
    gap: var(--z-space-4);
    justify-content: center;
  }
  .floatstrip__ph { position: static; inset-inline-start: auto !important; }
}

/* ── ROTATION · treatment D ───────────────────────────────
   The live card is announced twice, quietly: the others fade back, and it
   lifts off the row. Both are on the CARD so the whole thing moves as one
   object. The rail underneath says which of the set you are on, because
   once the neighbours are faded it stops being obvious how many there are. */
.format-grid > .card { transition: opacity var(--z-dur-med, .3s) var(--z-ease-out),
                                   transform var(--z-dur-med, .3s) var(--z-ease-out); }
/* Specificity, not preference: reveal.js paints opacity:1 via
   `html.js-reveal [data-reveal].is-visible` (0,3,1), which outranks a plain
   `.format-grid > .card` rule (0,3,0) — the fade silently did nothing until
   it was scoped to outrank the reveal. Measured: a non-live card computed
   opacity 1. The unscoped selector stays for the no-JS path. */
/* :has() guard, and it is load-bearing. If nothing is centred — before the
   rotation settles, or when the track is scrolled off vertically — nearest()
   returns null and NO card is live, at which point an unguarded rule fades
   every card at once and the section reads as broken. Observed on the
   Arabic page. Fade only while there is something to fade FOR. */
html.js-reveal .format-grid:has(> .card.is-live) > .card[data-reveal]:not(.is-live),
.format-grid:has(> .card.is-live) > .card:not(.is-live) { opacity: .45; }
html.js-reveal .format-grid > .card[data-reveal].is-live,
.format-grid > .card.is-live { transform: translateY(-10px); }

.mslot-rail {
  display: flex;
  gap: 6px;
  margin-block-start: var(--z-space-4);
}
.mslot-rail i {
  block-size: 4px;
  flex: 1;
  background: var(--z-chart-track);
  transition: background-color var(--z-dur-med, .3s) var(--z-ease-out);
}
.mslot-rail i.is-loaded { background: var(--z-gold); }
.mslot-rail i.is-live   { background: var(--z-jam); }

/* Reduced motion: keep the information, drop the movement. The fade and the
   rail still say which card is live; only the lift and the easing go. */
@media (prefers-reduced-motion: reduce) {
  .format-grid > .card { transition: none; }
  html.js-reveal .format-grid > .card[data-reveal].is-live,
  .format-grid > .card.is-live { transform: none; }
  .mslot-rail i { transition: none; }
}

.mslot[data-media-near][data-media-state="deferred"] {
  border-color: var(--z-jam);
  background: var(--z-churn-deep, var(--z-creme));
}
@media (prefers-reduced-motion: no-preference) {
  .mslot[data-media-near] { transition: border-color .25s ease, background .25s ease; }
}

/* ── media slots ─────────────────────────────────────────
   One box, three states, written so the EMPTY state is the default. A slot
   with nothing configured must read as deliberate rather than broken —
   that is the whole reason this is a hatched, dashed, labelled frame and
   not a grey rectangle waiting for an image.

   9:16 is enforced on the slot, not the video, so a vertical reel can
   never stretch a card. That is the containment strategy: the card width
   decides, the aspect ratio follows, and desktop never sees a 1080-tall
   video pushing a row apart. */
.mslot {
  position: relative;
  aspect-ratio: 9 / 16;
  margin-block-end: var(--z-space-3);
  /* NO CUT ON THE MEDIA. The chamfer is this site's signature on things you
     press and things that hold content — buttons, tabs, cards. A photograph
     is neither, and clipping an image's corners does not decorate it, it
     removes part of it, which reads as damage rather than as a decision.
     It also doubled up: a cut frame inside a cut card put the same angle
     twice within 16px, which is where it stops looking deliberate.

     The CARD around this keeps the cut, so the shape still reads. The
     picture inside it stays whole — the usual rule for a signature shape
     is that it goes on the container, never on the artwork.

     overflow:hidden stays; it is what crops the embed's chrome. */
  border-radius: 2px;
  overflow: hidden;
  background: var(--z-churn);
  display: grid;
  place-items: center;
}
/* EMPTY — no file, no permalink. Hatching says "intentionally nothing
   here yet" in a way a flat fill cannot. */
.mslot:not([data-media-state]) {
  border: 1px dashed var(--z-hairline-strong);
  background:
    repeating-linear-gradient(135deg, rgb(92 14 44 / .05) 0 8px, transparent 8px 16px),
    var(--z-churn);
}
.mslot:not([data-media-state])::after {
  content: attr(data-media);
  font-family: var(--z-font-label);
  font-size: 9px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--z-fg-faint);
}

.mslot video,
.mslot img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
}
.mslot__link { display: block; inline-size: 100%; block-size: 100%; }

/* Play badge. One shape for both the link state and the reduced-motion
   button, so there is a single thing to recognise. */
.mslot__play {
  position: absolute;
  inset-block-end: var(--z-space-3);
  inset-inline-start: var(--z-space-3);
  inline-size: 2.25rem;
  block-size: 2.25rem;
  border: 0;
  border-radius: 2px;
    clip-path: var(--z-chamfer-sm);
  background: var(--z-jam);
  cursor: pointer;
  display: grid;
  place-items: center;
}
/* Triangle drawn with borders rather than a glyph: no font dependency, and
   it stays optically centred at any size. Nudged right by 2px because a
   triangle's visual centre is left of its bounding box. */
.mslot__play::before {
  content: "";
  inline-size: 0;
  block-size: 0;
  margin-inline-start: 3px;
  border-block: 6px solid transparent;
  border-inline-start: 10px solid var(--z-churn);
  border-inline-end: 0;
}
[dir="rtl"] .mslot__play::before { transform: scaleX(-1); }
.mslot__play--btn:focus-visible { outline: 2px solid var(--z-focus-ring); outline-offset: 2px; }

/* ── formats: buyer-side outcome line ────────────────────
   The card's own line says what the format IS; this says what the brand
   GETS. Kept as a separate element rather than folded into the description
   so the two register differently — the outcome is the part a media buyer
   is scanning for, and it should not read as more description. */
.format__gets {
  margin-block-start: var(--z-space-3);
  padding-block-start: var(--z-space-3);
  border-block-start: 1px solid var(--z-hairline);
  font-size: var(--z-text-small);
  font-weight: 600;
  color: var(--z-fg);
}


/* ── formats ────────────────────────────────────────────── */
.format-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(var(--z-format-card-min-w), 1fr));
  gap: var(--z-space-4);
  list-style: none;
  margin-block-start: var(--z-space-12);
}
.format { background: var(--z-paper); }

/* ── formats on touch / narrow: a real swipe carousel ──────
   Below the width where the pinned scrub earns its keep, eight cards in a
   one-column grid is a 2,100px wall to scroll past. This turns the same
   markup into a native horizontally-scrollable, snapping row — the phone
   pattern people already know. It is plain overflow scrolling: no JS, no
   scroll-jacking, momentum and rubber-banding all stay native, and it is
   what no-JS and reduced-motion get too, since none of them carry
   data-pin-state either.

   min(78vw, 20rem) is the whole responsive story: ~1.3 cards on a phone,
   ~2.4 on a tablet, ~3.2 by the time the pinned layout takes over — always
   landing on a partial card, so there is a visible edge inviting the
   swipe at every size. */
/* ── THE DECK ─────────────────────────────────────────────
   A stack of postcards, not a row. One card face up, the rest fanned
   behind it; dealing throws the top one and lifts the next.

   Absolutely positioned children inside a fixed-ratio stage, so the stack
   has one footprint no matter how many cards it holds and nothing reflows
   when a card is dealt. --d is the depth from the top, set per card by
   reels.js; every transform below is a function of it, which is what keeps
   eight cards to four rules. */
.formats-pin:not([data-pin-state]) .format-grid.is-deck {
  display: block;
  position: relative;
  inline-size: min(100%, 24rem);
  margin-inline: auto;
  /* 3/5, not 3/4.5. The reels are portrait; at the shorter ratio the media
     frame was squeezed to 382x409 — 0.93, essentially square — and a
     portrait video in a square hole can only letterbox or crop the face
     out of shot. Taller stage, so the frame that holds the video is
     portrait too. */
  aspect-ratio: 3 / 5;
  overflow: visible;
  cursor: pointer;
  touch-action: pan-y;              /* let the page scroll; we take sideways */
}
.formats-pin:not([data-pin-state]) .format-grid.is-deck > .format {
  position: absolute;
  inset: 0;
  margin: 0;
  /* A HAND OF CARDS, fanned from the bottom edge. The previous version
     stepped the stack straight down the y axis with no rotation, which
     meant every card behind hid inside the silhouette of the one in front
     and the section read as a single panel — which is exactly what it
     looked like. Fanning needs three things moving together: a sideways
     step so the edges clear, a rotation about the BOTTOM of the card so
     the top corners spread the way a held hand spreads, and a scale so
     the far cards sit back in space.

     Every term is a function of --d, so the fan is correct for a hand of
     four or of forty without another rule being written. */
  transform:
    translateX(calc(var(--d, 0) * var(--fan-x, 15px)))
    translateY(calc(var(--d, 0) * -4px))
    rotate(calc(var(--d, 0) * 2.4deg))
    scale(calc(1 - var(--d, 0) * 0.04));
  transform-origin: 50% 100%;
  /* No opacity here. This selector is (0,5,1); the depth-fade formula
     below is (0,4,1), so an `opacity: 1` on this rule silently beat it and
     every card in the stack rendered solid. The old enumerated rules only
     worked because their [style*="--d: 4"] attribute selector happened to
     match this specificity. Depth opacity lives in one place now. */
  transition:
    transform .46s cubic-bezier(.22,.72,.24,1),
    opacity .3s ease,
    filter .3s ease;
  will-change: transform;
  /* filter, NOT box-shadow. .card carries clip-path for the knife cut, and
     clip-path clips box-shadow out of existence — so every shadow written
     on these cards had been painting nothing at all, which is half the
     reason the stack was invisible. drop-shadow() follows the clipped
     silhouette instead, so the fan gets its separation back AND the shadow
     picks up the cut corners.

     No `box-shadow: none` here either, tempting as it was: this selector is
     (0,5,0) and the gold hairline on the card backs below is (0,4,0), so
     resetting it here would have silently erased it — the same specificity
     trap that hid the depth fade. Nothing else sets box-shadow on these,
     so there is nothing to reset. */
  filter: drop-shadow(0 10px 16px rgb(92 14 44 / .16));
}
/* THE CARD BACKS. Only the face card shows its reel and its title; the
   ones behind show blank stock. This is the difference between a deck and
   a mess: real cards in a hand are opaque, so what spreads behind the top
   one is card, not a half-legible second copy of the layout. It also means
   the fan can be as wide as it likes without any text peeking out.

   Contents are hidden with opacity rather than visibility so the reel in
   the card underneath keeps its layout and does not re-measure when it is
   dealt to the top. */
.format-grid.is-deck > .format:not(.is-face) { pointer-events: none; }
.format-grid.is-deck > .format:not(.is-face) > * {
  opacity: 0;
  transition: opacity .22s ease;
}
/* A hairline inset along the top of each back, so a blank card still reads
   as printed stock rather than as a grey rectangle. Inset, because an
   outer ring would be clipped by the chamfer the same way the old shadow
   was. */
.format-grid.is-deck > .format:not(.is-face) {
  box-shadow: inset 0 3px 0 0 var(--z-gold);
}
/* Depth fade as a FORMULA, not a list. This used to enumerate --d: 4
   through 7, so adding a ninth card gave it a depth nobody had written a
   rule for and it rendered at full opacity on top of the stack. One
   expression covers any number of cards:

     d0-d2 solid, d3 half, d4 and deeper gone.

   Three solid backs is what makes it read as a deck — at the old d2 = half
   there was effectively one card visible.

   Scoped under html.js-reveal because the grid carries data-stagger, and
   reveal.js paints opacity:1 through
   `html.js-reveal [data-stagger] [data-reveal].is-visible` at (0,4,1) —
   which outranks an unscoped rule here. */
html.js-reveal .format-grid.is-deck > .format,
.format-grid.is-deck > .format {
  opacity: clamp(0, calc(2 - var(--d, 0) * 0.5), 1);
}
html.js-reveal .format-grid.is-deck > .format.is-face,
.format-grid.is-deck > .format.is-face { opacity: 1; }
html.js-reveal .format-grid.is-deck > .format.is-leaving,
.format-grid.is-deck > .format.is-leaving { opacity: 0; }
.formats-pin:not([data-pin-state]) .format-grid.is-deck > .format.is-face {
  transform: none;
  filter: drop-shadow(0 18px 26px rgb(92 14 44 / .22));
}
/* SCRUBBING. Running the pointer down the rail can cross four cards in a
   flick, and the deal animation is built for one step — four throws queued
   back to back reads as a stutter, not as speed. While the rail is being
   scrubbed the cards just move to their new depths, faster and without the
   throw, so the stack flows under the pointer.

   Scoped through .formats-pin because the rule it has to beat — the one
   that sets the .46s transition — is (0,5,0), and (0,3,0) here would have
   lost silently. */
.formats-pin:not([data-pin-state]) .format-grid.is-deck.is-scrubbing > .format {
  transition-duration: .2s, .18s, .18s;
}

/* THE DRAG. While a finger or pointer is down, the face card tracks it —
   --drag is written by reels.js in px. A card you can push is the whole
   affordance; without it the only way to know the stack moves is to guess.
   Transition off during the drag so it tracks 1:1, and the tilt is derived
   from the distance so the card leans into the throw. */
.format-grid.is-deck > .format.is-dragging {
  transition: none;
  /* --drag is a UNITLESS number of pixels, not a length. A length cannot be
     multiplied into an angle — `calc(12px * 0.02)` is 0.24px, not 0.24deg —
     so the tilt would have been dropped as invalid. The unit is applied
     here, once per term. */
  transform:
    translateX(calc(var(--drag, 0) * 1px))
    rotate(calc(var(--drag, 0) * 0.022deg))
    !important;
}
/* ── THE CARD FACE: caption above, picture below ───────────
   The title used to sit under the reel, which made the card read bottom
   up: you met the picture with no idea what it was an example of, then
   found the label after. A postcard is captioned at the top. Moving it
   there also puts the two smallest elements — the icon and the word —
   on one line, so the picture gets the whole rest of the card instead of
   competing with a stacked header.

   Done with grid areas rather than by reordering the markup, because the
   source order (icon, media, heading) is the right reading order for a
   screen reader and should not be bent to serve a visual arrangement. */
.formats-pin:not([data-pin-state]) .format-grid.is-deck > .format {
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-rows: auto minmax(0, 1fr);
  /* No column-gap. The gold rule runs along the bottom of BOTH cells, and a
     gap between them punches a notch in it — the line has to be unbroken
     or it reads as two underlines. The breathing room moves to padding on
     the heading instead, which sits inside the ruled box. */
  column-gap: 0;
  align-content: stretch;
}
.format-grid.is-deck > .format > .format__icon {
  grid-area: 1 / 1;
  align-self: stretch;
  inline-size: 1.75rem;
  block-size: auto;
  margin: 0;
  padding-block-end: var(--z-space-3);
}
.format-grid.is-deck > .format > h3 {
  grid-area: 1 / 2;
  display: flex;
  align-items: center;
  margin: 0;
  /* The old caption plate put a gold rule ABOVE the heading, back when the
     heading sat under the reel. The rule moved to the underside when the
     caption moved to the top — but the old one was never cleared, so the
     title came out sandwiched between two gold lines. */
  border-block-start: 0;
  padding-block: 0 var(--z-space-3);
  padding-inline: var(--z-space-3) 0;
  font-size: 1.375rem;
  line-height: 1.15;
}
/* The gold rule runs under BOTH cells, so it reads as one line across the
   card rather than as two underlines that happen to meet. */
.format-grid.is-deck > .format > .format__icon,
.format-grid.is-deck > .format > h3 {
  border-block-end: 2px solid var(--z-gold);
}
/* The picture takes every pixel the caption did not. No aspect-ratio: the
   stage above already fixes the card at 3/5, so letting the frame fill the
   remaining row keeps the crop honest at any card size instead of leaving
   a band of dead paper under a fixed 9/16 box. */
.format-grid.is-deck > .format > .mslot {
  grid-area: 2 / 1 / 3 / 3;
  aspect-ratio: auto;
  /* Stretch, not block-size:100%. A percentage height on a grid item
     resolves against the grid AREA, and the margin above is then added on
     top of it — so the frame hung 12px past the card's bottom padding. The
     default stretch already fills the area and accounts for the margin. */
  min-block-size: 0;
  margin: var(--z-space-3) 0 0;
}

/* THE THROW. The card being replaced leaves to the left with a little
   tilt, then rejoins the stack once it is out of sight. This is the whole
   difference between a deal and a jump cut. */
.format-grid.is-deck > .format.is-leaving {
  transform: translateX(-118%) rotate(-7deg) scale(.96) !important;
  opacity: 0;
  transition:
    transform .46s cubic-bezier(.4,0,.7,1),
    opacity .34s ease-in;
  z-index: 20 !important;
}
@media (prefers-reduced-motion: reduce) {
  .format-grid.is-deck > .format.is-leaving { transition: none; }
}
/* The idle float, on the face card only and small enough to read as weight
   rather than as motion. Paused while a card is being thrown so the two
   transforms cannot fight. */
@media (prefers-reduced-motion: no-preference) {
  .format-grid.is-deck > .format.is-face { animation: deckFloat 7s ease-in-out infinite; }
  .format-grid.is-deck > .format.is-leaving { animation: none; }
  @keyframes deckFloat {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-6px); }
  }
}
.format-grid.is-deck:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--z-focus-ring); }

/* ── THE RAIL: a scrub strip, not nine small targets ──────
   The rail is the deck's readout AND the fastest way through it, so
   running the pointer along it deals as it goes — no click. That needs a
   hit area you cannot miss, which a 4px bar is not.

   So the BUTTON is 22px tall and invisible, and the bar people see is
   drawn by its ::after. The target is five times the height of the mark,
   the marks keep their original weight, and the gaps between them stay
   inside the rail so a pointer crossing one never falls into a dead zone.
   touch-action:none so dragging a finger along the strip scrubs instead of
   scrolling the page out from under it — 22px is a small enough band to
   take that from the page without it being felt anywhere else. */
.mslot-rail {
  display: flex;
  gap: 6px;
  margin-block-start: var(--z-space-4);
  inline-size: min(100%, 26rem);
  margin-inline: auto;
  cursor: pointer;
  touch-action: none;
}
.mslot-rail button {
  flex: 1;
  position: relative;
  block-size: 22px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.mslot-rail button::after {
  content: "";
  position: absolute;
  inset-block: 9px;
  inset-inline: 0;
  background: var(--z-chart-track);
  transition:
    background-color var(--z-dur-med, .3s) var(--z-ease-out),
    transform var(--z-dur-med, .3s) var(--z-ease-out);
}
.mslot-rail button.is-loaded::after { background: var(--z-gold); }
/* The live mark thickens rather than only changing colour: at 4px a hue
   change alone is most of a hairline away from being invisible, and the
   rail has to say where you are from across the section. */
.mslot-rail button.is-live::after { background: var(--z-jam); transform: scaleY(2.5); }
.mslot-rail button:focus-visible { outline: 2px solid var(--z-focus-ring); outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) {
  .mslot-rail button::after { transition: none; }
}

/* ── grid children: distribute the slack, don't dump it at the bottom ──
   These all sit in grids that stretch every child to the tallest one in
   the row. Left as block/list-item they have no vertical distribution, so
   a short card's content packs against the top and the whole difference
   piles up underneath — 243px of dead space under the Gender panel, which
   has one bar next to neighbours with six. Centring is a no-op when the
   content fills the box and fixes it when it doesn't.

   .step is deliberately NOT in this list: it hangs from a gold top rule,
   so its content is meant to sit against the top edge. */
.audience__panel,
.format,
.reel {
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.format:hover { border-color: var(--z-jam); }
.format__icon {
  inline-size: 2.75rem;
  block-size: 2.75rem;
  margin-block-end: var(--z-space-4);
}
.format h3 { font-size: var(--z-text-h2); margin-block-end: var(--z-space-2); }
.format p { font-size: var(--z-text-small); color: var(--z-fg-muted); }
.format--feature { border-color: var(--z-gold); border-width: 2px; }

.approaches { margin-block-start: var(--z-space-12); }
/* The chips are inside the pinned band now, and the band's own flex gap
   supplies the spacing. Scoped to the pinned case so the unpinned
   CSS-Grid fallback (narrow viewports, no-JS, reduced motion) keeps the
   gap it was designed with. */
.formats-pin[data-pin-state] .approaches { margin-block-start: 0; }
.approaches__title {
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  font-weight: var(--z-weight-medium);
  letter-spacing: var(--z-tracking-label);
  text-transform: uppercase;
  color: var(--z-fg-muted);
  margin-block-end: var(--z-space-4);
}
.chips { display: flex; flex-wrap: wrap; gap: var(--z-space-2); list-style: none; }
.chip {
  padding-block: var(--z-space-2);
  padding-inline: var(--z-space-4);
  border: 1px solid var(--z-hairline-strong);
  border-radius: 2px;
    clip-path: var(--z-chamfer-sm);
  font-size: var(--z-text-small);
  color: var(--z-fg-muted);
}

/* ── proof ──────────────────────────────────────────────── */
.reel-grid,
.engagement-grid {
  display: grid;
  gap: var(--z-space-4);
  list-style: none;
  margin-block-start: var(--z-space-8);
}
.reel-grid { grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }
.engagement-grid { grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); }

.reel { text-align: center; background: var(--z-gold); border-color: transparent; }
.reel__views {
  display: block;
  font-family: var(--z-font-display);
  font-size: var(--z-text-h2);
  font-weight: var(--z-weight-black);
  letter-spacing: var(--z-tracking-h1);
  color: var(--z-jam);
}
.reel__label {
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  letter-spacing: var(--z-tracking-label);
  text-transform: uppercase;
  color: var(--z-jam);
  opacity: .7;
}
.engagement dl {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--z-space-3);
  text-align: center;
}
.engagement dt {
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  color: var(--z-fg-faint);
  text-transform: uppercase;
  letter-spacing: .08em;
}
.engagement dd {
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-bold);
  font-size: var(--z-text-body);
  margin-block-start: var(--z-space-1);
}
.proof__note {
  margin-block-start: var(--z-space-6);
  font-size: var(--z-text-small);
  color: var(--z-fg-muted);
  max-inline-size: 56ch;
}

/* ── games pointer (funnel page) ─────────────────────────── */
.games-pointer__link {
  display: flex;
  align-items: center;
  gap: var(--z-space-4);
  padding: var(--z-space-6);
  border: 2px solid var(--z-hairline-strong);
  border-radius: 2px;
  clip-path: var(--z-chamfer);
  background: var(--z-creme);
  transition: border-color var(--z-dur-fast) var(--z-ease-out),
              background-color var(--z-dur-fast) var(--z-ease-out);
}
.games-pointer__link:hover { border-color: var(--z-jam); background: var(--z-paper); }
.games-pointer__mark {
  inline-size: 2.5rem;
  block-size: 2.5rem;
  flex-shrink: 0;
  /* The icon is a <use> against the shared #i-format-game symbol (also
     used, unanimated, in the nav tab), so its internal shapes are not
     individually selectable from here — a <use> shadow tree is closed to
     descendant selectors. Rocking the WHOLE mark instead, pivoted low
     (74% down, where the joystick's base sits), reads as the stick-and-ball
     swinging on its base rather than the base itself moving — a hand
     working the stick back and forth. */
  transform-origin: 50% 74%;
  animation: joystickSway var(--z-dur-joystick) var(--z-ease-in-out) infinite;
}
@keyframes joystickSway {
  0%, 100% { transform: rotate(-9deg); }
  50%      { transform: rotate(9deg); }
}
/* Decoration, not information — reduced motion stops it outright rather
   than merely slowing it. */
@media (prefers-reduced-motion: reduce) {
  .games-pointer__mark { animation: none; }
}
.games-pointer__text { display: flex; flex-direction: column; gap: var(--z-space-1); flex: 1; }
.games-pointer__eyebrow {
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  letter-spacing: var(--z-tracking-label);
  text-transform: uppercase;
  color: var(--z-fg-muted);
}
.games-pointer__title { font-weight: var(--z-weight-medium); }
.games-pointer__cta {
  font-weight: var(--z-weight-bold);
  font-size: var(--z-text-small);
  white-space: nowrap;
}
@media (max-width: 767px) {
  .games-pointer__link { flex-wrap: wrap; }
  .games-pointer__cta { inline-size: 100%; }
}

/* ── process ────────────────────────────────────────────── */
/* ── the roadmap ─────────────────────────────────────────
   The process steps as a route the reader travels: a rail down the
   inline-start edge that fills with Melt Gold as the section scrolls, and
   three stations that set as the spread reaches them.

   VERTICAL, not the old three-column grid. A fill that tracks scroll only
   reads if it runs the same direction the reader is moving; across three
   columns it would be a progress bar lying on its side. It also means the
   phone and the desktop get the same object rather than two layouts.

   Everything directional is logical, so the rail moves to the right and
   the stations flip with it in Arabic, with no override. */
.roadmap {
  position: relative;
  list-style: none;
  margin-block-start: var(--z-space-12);
  /* Two numbers drive the whole vertical layout, so the rail, the stations
     and the copy can never drift apart the way three separate hardcoded
     calcs could:
       --rm-rail   where the rail and the station centres sit
       --rm-indent where the copy starts
     The distance between them IS the breathing room beside each circle:
     indent - rail - half a node = 5.5 - 3.125 - 1.375 = 1rem of clear
     space, where it used to be exactly 0 and the text sat against the
     circle's edge. */
  --rm-rail: 3.125rem;
  --rm-indent: 5.5rem;
  padding-inline-start: var(--rm-indent);
  display: flex;
  flex-direction: column;
  gap: var(--z-space-16);
}
.roadmap__rail {
  position: absolute;
  /* Ends at the last station rather than running on to the bottom of its
     copy. pin-scroll.js measures both ends off the real station boxes and
     writes them here; the 0/100% fallbacks are what no-JS renders. */
  inset-block-start: var(--rail-start, 0px);
  inset-block-end: auto;
  block-size: var(--rail-span, 100%);
  inset-inline-start: calc(var(--rm-rail) - 2px);
  inline-size: 4px;
  border-radius: var(--z-radius-full);
  background: var(--z-hairline);
  overflow: visible;
}
/* --road-progress is written by roadmap.js, 0 -> 1. scaleY on a
   transform-origin at the block start keeps this on the compositor —
   animating block-size would relayout the whole section every frame. */
.roadmap__fill {
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 100%;
  border-radius: inherit;
  /* Solid, not a gradient. The element is SCALED rather than resized, so any
     gradient compresses into the filled portion and parks its far end right
     under the bead — a dark smear that read as an artefact. Melt Gold is a
     fill in this system anyway; the Jam bead supplies the accent. */
  background: var(--z-gold);
  transform: scaleY(var(--road-progress, 0));
  transform-origin: top;
  transition: transform var(--z-dur-fast) linear;
}
/* The leading edge — a bead of butter at the tip of the spread, so the
   rail reads as something being drawn rather than a bar being filled. */
.roadmap__tip {
  position: absolute;
  inset-inline-start: 50%;
  inset-block-start: calc(var(--road-progress, 0) * 100%);
  inline-size: 1rem;
  block-size: 1rem;
  margin-inline-start: -.5rem;
  margin-block-start: -.5rem;
  border-radius: var(--z-radius-full);
  background: var(--z-jam);
  opacity: var(--road-tip, 0);
  transition: inset-block-start var(--z-dur-fast) linear,
              opacity var(--z-dur-base) var(--z-ease-out);
}

.roadmap__step {
  position: relative;
  display: flex;
  align-items: flex-start;
  gap: var(--z-space-6);
}
.roadmap__node {
  position: absolute;
  /* Centred on the rail: back out of the copy's indent, then step in to
     the rail and back off by half a node. Derived from the same two
     variables the indent uses, so moving either keeps circle, rail and
     copy in register. */
  inset-inline-start: calc(var(--rm-rail) - var(--z-roadmap-node) / 2 - var(--rm-indent));
  inset-block-start: 0;
  display: grid;
  place-items: center;
  inline-size: var(--z-roadmap-node);
  block-size: var(--z-roadmap-node);
  border-radius: var(--z-radius-full);
  background: var(--z-bg);
  border: 3px solid var(--z-hairline-strong);
  transition: background-color var(--z-dur-base) var(--z-ease-out),
              border-color var(--z-dur-base) var(--z-ease-out),
              transform var(--z-dur-base) var(--z-ease-spread);
}
.roadmap__num {
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-black);
  font-size: var(--z-text-body);
  color: var(--z-fg-muted);
  transition: color var(--z-dur-base) var(--z-ease-out);
}
/* Set: the spread has reached this station. */
.roadmap__step.is-reached .roadmap__node {
  background: var(--z-gold);
  border-color: var(--z-jam);
  transform: scale(1.08);
}
.roadmap__step.is-reached .roadmap__num { color: var(--z-jam); }

.roadmap__body { padding-block-start: var(--z-space-1); }
.roadmap__body h3 {
  font-size: var(--z-text-h2);
  margin-block-end: var(--z-space-2);
  transition: color var(--z-dur-base) var(--z-ease-out);
}
.roadmap__body p { font-size: var(--z-text-body); color: var(--z-fg-muted); max-inline-size: 46ch; }

@media (min-width: 768px) {
  /* Wider gutter at this size; rail and node follow the variables, so the
     two rules that used to restate the same arithmetic are gone. */
  .roadmap { --rm-rail: 4rem; --rm-indent: 6.5rem; gap: var(--z-space-24); }
}

/* No JS, or reduced motion: the route is already complete. The steps are
   content, and content is never gated behind an animation. data-road-static
   is gone — that was roadmap.js's own attribute, and roadmap.js no longer
   exists (see pin-scroll.js below). html:not(.js-reveal) is set globally by
   an inline script in head.html independent of any one component, so it
   still correctly identifies "no JS, or motion not permitted" here. */
html:not(.js-reveal) .roadmap__fill { transform: scaleY(1); }
@media (prefers-reduced-motion: reduce) {
  .roadmap__fill { transform: scaleY(1); transition: none; }
  .roadmap__tip { display: none; }
  .roadmap__node { transition: none; }
}

/* ============================================================
   HORIZONTAL PIN-SCRUB — .process (roadmap) + .formats
   assets/js/pin-scroll.js

   Shared contract: [data-pin] > [data-pin-sticky] > [data-pin-track].
   With NO data-pin-state attribute (no-JS, reduced-motion, IO unsupported,
   or "measured and it already fits") every rule below is inert — .process
   renders as the plain vertical roadmap above, .formats as the plain CSS
   Grid below, untouched. data-pin-state is only ever written by JS AFTER
   a successful measurement, so there is no code path that can add height
   or motion before JS has confirmed both are warranted. See pin-scroll.js
   for the measurement/progress math; this is layout only. */

/* ── .process, pinned or "fit" (measured horizontal, no overflow) ── */
.roadmap-pin[data-pin-state] .pin__sticky {
  /* Fills the screen under the header and holds the WHOLE section —
     heading and stations together. That is what removes the dead space:
     the lock's scroll runway lives below this box, so any part of the
     viewport this box does not cover would show through as emptiness for
     the entire lock. Covering it means the locked screen is just the
     section, sitting still. Scoped to [data-pin-state] so pin-scroll.js
     measures the expanded box and the pin's block-size accounts for it. */
  min-block-size: calc(100dvh - var(--z-header-h));
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--z-space-6);
}
.roadmap-pin[data-pin-state] .roadmap {
  flex-direction: row;
  align-items: stretch;
  gap: var(--z-space-8);
  padding-inline-start: 0;
  /* The vertical layout's margin-block-start (spacing under the section
     title) is redundant here, and worse, it's measured inconsistently: it
     collapses outward through .pin__sticky in "fit" (overflow:visible, no
     BFC) but gets trapped inside it in "pinned" (overflow:hidden creates
     one), growing the sticky box by 48px AFTER pin-scroll.js already
     measured and sized it. That mismatch made the pin release ~1/3 of the
     way through the scrub instead of at the end. */
  margin-block-start: 0;
}
.roadmap-pin[data-pin-state] .roadmap__step {
  /* All three share the row and always fit — nothing scrolls sideways here.
     The lock exists so the line can draw across a stationary section, so a
     station leaving the viewport would defeat the point. min-inline-size:0
     is what lets a flex item actually shrink below its text's min-content. */
  flex: 1 1 0;
  min-inline-size: 0;
  flex-direction: column;
  align-items: flex-start;
}
/* The copy dims until the spread reaches its station — this is the "reveal"
   half of the scrub. Deliberately on .roadmap__body and NOT on the step:
   fading the whole step also fades the circle, and a translucent circle
   stops hiding the bead that's supposed to slide behind it. The stations
   themselves stay fully opaque, which is what a stepper wants anyway — you
   can see the whole route, and the copy arrives as you reach each stop. */
.roadmap-pin[data-pin-state] .roadmap__body {
  opacity: .32;
  transition: opacity var(--z-dur-base) var(--z-ease-out);
}
.roadmap-pin[data-pin-state] .roadmap__step.is-reached .roadmap__body { opacity: 1; }

/* The rail is now a horizontal line through the middle of the circles,
   running from the outer edge of station 1 to the outer edge of the last
   one. Both ends come from --rail-start/--rail-span, measured off the real
   station boxes by pin-scroll.js rather than re-derived from step widths
   and gaps here, so the line cannot drift out of registration with the
   circles it threads. The circles paint over it (they're --z-layer-raised
   below) so it reads as connecting them, not crossing them. */
.roadmap-pin[data-pin-state] .roadmap__rail {
  inset-inline-start: var(--rail-start, 0px);
  inset-inline-end: auto;
  inset-block-start: calc(var(--z-roadmap-node) / 2 - var(--z-roadmap-rail) / 2);
  inset-block-end: auto;
  inline-size: var(--rail-span, 100%);
  block-size: var(--z-roadmap-rail);
}
.roadmap-pin[data-pin-state] .roadmap__fill {
  inset-block: 0;
  inset-inline: 0;
  inline-size: 100%;
  block-size: 100%;
  transform: scaleX(var(--road-progress, 0));
  /* transform-origin has no reliably-supported logical keyword yet, so this
     is the one place here that needs an explicit dir override — same
     precedent as .icon--dir { transform: scaleX(-1) } elsewhere. */
  transform-origin: 0 0;
  /* Un-transitioned for the same reason as the track transform below: the
     fill is scrubbed, not animated, and 140ms of easing on a scrubbed value
     reads as the line lagging behind the scrollbar. */
  transition: none;
}
html[dir="rtl"] .roadmap-pin[data-pin-state] .roadmap__fill { transform-origin: 100% 0; }
.roadmap-pin[data-pin-state] .roadmap__tip {
  /* Centred on the rail (the -.5rem block margin from the base rule makes
     50% mean "centre"), riding the fill's leading edge. Visibility is left
     to the base rule's opacity: var(--road-tip) — pin-scroll.js drops it to
     0 at both ends of the run, because at 0% and 100% the bead is centred
     on the outer edge of a terminal station and half of it hangs off into
     open space. Only the position is un-transitioned; opacity still eases,
     so the bead fades rather than blinking out. */
  inset-block-start: 50%;
  inset-inline-start: calc(var(--road-progress, 0) * 100%);   /* logical — free in RTL */
  inset-inline-end: auto;
  transition: opacity var(--z-dur-base) var(--z-ease-out);
}
.roadmap-pin[data-pin-state] .roadmap__node {
  /* relative, not static, only so it paints over the rail it sits on.
     The insets MUST be cleared with it: the vertical layout hangs the node
     out to the side with a negative inset-inline-start, which `static`
     ignores and `relative` would silently start honouring — shifting every
     circle ~54px off the line the rail was measured against. */
  position: relative;
  inset-inline-start: auto;
  inset-block-start: auto;
  z-index: var(--z-layer-raised);
  margin-block-end: var(--z-space-4);
}

/* "fit": already horizontal, nothing overflows, so there is nothing to
   scrub — the task's own example (three steps side by side on a wide
   screen). Rail renders fully filled, same "route is already complete"
   philosophy as the no-JS/reduced-motion rule above. */
.roadmap-pin[data-pin-state="fit"] .roadmap__fill { transform: scaleX(1); }
.roadmap-pin[data-pin-state="fit"] .roadmap__step { --road-progress: 1; }

/* "pinned": the scroll-lock itself. An explicit block-size is the ONLY
   thing that makes [data-pin] taller than its sticky child — set as an
   inline style by JS, never in this stylesheet, which is what keeps a
   no-JS page from ever rendering a dead-space gap. (Not padding-block-end:
   position:sticky does not extend its stuck range into its own containing
   block's padding, in any browser — only real box height counts. That
   makes this rule a deliberate no-op left unset here, not an oversight.) */
.roadmap-pin[data-pin-state="pinned"] .pin__sticky {
  /* Standing invariant, not a one-time check: iOS Safari silently breaks
     position:sticky if ANY ancestor between this element and the true
     scroll root gets non-visible overflow, or a transform/filter/
     will-change/contain that creates a new containing block. Keep body,
     main, .section and .roadmap-pin itself free of all of those. */
  position: sticky;
  inset-block-start: var(--z-header-h);
  overflow: hidden;
  z-index: var(--z-layer-sticky);   /* 10 — documented "subordinate to header" (20) */
}
/* No transform on the roadmap track, deliberately: unlike .formats, this
   section does not travel sideways. The scroll drives the line and the
   stations, and the steps themselves hold still and centred for the whole
   lock so the full section stays on screen throughout. */
.roadmap-pin[data-pin-state="pinned"] .roadmap__step.is-reached .roadmap__node {
  background: var(--z-gold); border-color: var(--z-jam); transform: scale(1.08);
}
.roadmap-pin[data-pin-state="pinned"] .roadmap__step.is-reached .roadmap__num { color: var(--z-jam); }

/* ── .formats, pinned or "fit" ──
   No dedicated "fit" rules: if 8 cards ever fit an ultra-wide viewport
   with zero overflow, JS still writes data-pin-state="fit" and this shared
   flex-row rule alone renders them as one static row — correct by
   omission, nothing extra needed. */
.formats-pin[data-pin-state] .pin__sticky {
  container-type: inline-size;
  /* Fills the screen under the header and holds the whole section —
     heading, card row and chips. Same reasoning as .roadmap-pin above:
     whatever the sticky does not cover shows through as dead space under
     the band for the entire lock. */
  min-block-size: calc(100dvh - var(--z-header-h));
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--z-space-6);
}
.formats-pin[data-pin-state] .format-grid {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--z-space-4);
  /* Same fix as .roadmap-pin's .roadmap override above, same reason: this
     margin measures as 0 under "fit" (collapses outward, no BFC yet) and
     as 48px under "pinned" (overflow:hidden on .pin__sticky traps it) —
     a mismatch pin-scroll.js's single fit-time measurement can't see. */
  margin-block-start: 0;
}
.formats-pin[data-pin-state] .format-grid > .format {
  flex: 0 0 auto;
  /* 100/23.5 ≈ 4.26 cards: 4 full plus a ~26% peek of the 5th. Deliberately
     NOT an exact 4.0 — nothing may hijack the scroll gesture to teach the
     visitor more content exists, so the peek is the only affordance left
     to signal it, and it also sidesteps subpixel rounding producing a
     visible seam right at an exact quarter-width boundary. */
  inline-size: 23.5cqi;
  min-inline-size: 13rem;
}
.formats-pin[data-pin-state="pinned"] .pin__sticky {
  position: sticky;
  inset-block-start: var(--z-header-h);
  overflow: hidden;
  z-index: var(--z-layer-sticky);
}
.formats-pin[data-pin-state="pinned"] .format-grid {
  transform: translateX(calc(var(--pin-progress, 0) * var(--pin-distance, 0px) * var(--z-pin-sign)));
}

/* Belt-and-braces: the JS guard should already prevent data-pin-state from
   ever being written under reduced motion, so this should never be the
   thing that actually saves the page — but if it ever is, it forces the
   exact same static/complete layout as no-JS, not a half-pinned one. */
@media (prefers-reduced-motion: reduce) {
  .roadmap-pin, .formats-pin { block-size: auto !important; }
  .roadmap-pin[data-pin-state] .pin__sticky,
  .formats-pin[data-pin-state] .pin__sticky {
    position: static !important;
    overflow: visible !important;
    min-block-size: 0 !important;
    display: block !important;
  }
  [data-pin-track] { transform: none !important; }
  .roadmap-pin[data-pin-state] .roadmap__body { opacity: 1 !important; }
}

/* ── form wizard — one question per step ──────────────────
   Scoped under .form--wizard, which only JS adds. Without it every
   step renders stacked and the form is a normal long form. */
.fprogress,
.fcount,
.fnav { display: none; }
.form--wizard .fprogress,
.form--wizard .fcount { display: block; }
.form--wizard .fnav { display: flex; }

.form { margin-block-start: var(--z-space-12); }
.fprogress {
  block-size: 4px;
  border-radius: var(--z-radius-full);
  background: var(--z-chart-track);
  overflow: hidden;
  margin-block-end: var(--z-space-4);
}
.fprogress__bar {
  display: block;
  block-size: 100%;
  inline-size: var(--p, 0%);
  background: var(--z-gold);
  transition: inline-size var(--z-dur-base) var(--z-ease-out);
}
.fcount {
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  letter-spacing: var(--z-tracking-label);
  text-transform: uppercase;
  color: var(--z-fg-faint);
  margin-block-end: var(--z-space-8);
  font-variant-numeric: tabular-nums;
}

/* Height follows the content. This used to reserve 15rem to stop the
   form resizing between steps, but with a single short field per step
   that reserve rendered as a large empty band above the Next button. */
.form--wizard .fsteps { min-block-size: 0; }
.form--wizard .fstep { display: none; }
.form--wizard .fstep.is-active {
  display: block;
  animation: fstepIn var(--z-dur-base) var(--z-ease-out) both;
}
@keyframes fstepIn {
  from { opacity: 0; transform: translateY(var(--z-space-4)); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .form--wizard .fstep.is-active { animation: none; }
}

/* One question per screen — so the question is set at H2 size. */
.form--wizard .field > label,
.form--wizard .field > legend {
  font-family: var(--z-font-display);
  font-size: var(--z-text-h2);
  font-weight: var(--z-weight-bold);
  letter-spacing: var(--z-tracking-h1);
  margin-block-end: var(--z-space-4);
}

.field { margin-block-end: var(--z-space-6); border: none; }
.field > label,
.field > legend {
  display: block;
  margin-block-end: var(--z-space-2);
  font-size: var(--z-text-body);
  font-weight: var(--z-weight-medium);
}
/* §07: "Required is Gold, not a red asterisk." There is no red in the
   palette at all, so nothing about this form may reach for one. */
.field__req {
  display: inline-grid;
  place-items: center;
  inline-size: 1.1em;
  block-size: 1.1em;
  margin-inline-start: var(--z-space-2);
  border-radius: 2px;
    clip-path: var(--z-chamfer-sm);
  background: var(--z-gold);
  color: var(--z-jam);
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  font-weight: var(--z-weight-medium);
  text-indent: 0;
}
.field__hint {
  margin-block-start: var(--z-space-2);
  font-size: var(--z-text-small);
  color: var(--z-fg-faint);
}
/* §07 FIELD STATES:
     Rest     — Crème fill, hairline
     Focus    — border goes solid Jam, no glow
     Error    — Churn fill + the message, never red
     Disabled — 38% ink, dashed
   "There is no red in the palette, so errors are carried by fill
   change plus a written sentence." */
.field__error {
  margin-block-start: var(--z-space-2);
  font-size: var(--z-text-small);
  font-weight: var(--z-weight-medium);
  color: var(--z-jam);
  min-block-size: 1.4em;   /* reserved so showing an error can't shift layout */
}

.field input[type="text"],
.field input[type="email"],
.field select,
.field textarea {
  inline-size: 100%;
  padding-block: var(--z-space-3);
  padding-inline: var(--z-space-4);
  background: var(--z-creme);                    /* rest */
  border: 2px solid var(--z-hairline-strong);
  border-radius: 2px;
  clip-path: var(--z-chamfer);
  color: var(--z-fg);
  min-block-size: var(--z-hit-min);
  transition: border-color var(--z-dur-fast) var(--z-ease-out),
              background-color var(--z-dur-fast) var(--z-ease-out);
}
.field input::placeholder,
.field textarea::placeholder { color: var(--z-fg-faint); }
/* focus: solid Jam border, deliberately no glow */
.field input:focus,
.field select:focus,
.field textarea:focus { border-color: var(--z-jam); outline: none; }
.field textarea { resize: vertical; min-block-size: 8rem; }
/* error: the FILL changes and a sentence appears. No red anywhere. */
.field--invalid input,
.field--invalid select,
.field--invalid textarea {
  border-color: var(--z-jam);
  background: var(--z-churn);
}
.field input:disabled,
.field select:disabled,
.field textarea:disabled {
  color: var(--z-fg-faint);
  border-style: dashed;
  background: transparent;
  cursor: not-allowed;
}

.checks {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: var(--z-space-2);
}
.check {
  display: flex;
  align-items: center;
  gap: var(--z-space-3);
  padding-block: var(--z-space-3);
  padding-inline: var(--z-space-4);
  background: var(--z-paper);
  border: 2px solid var(--z-hairline-strong);
  border-radius: 2px;
    clip-path: var(--z-chamfer-sm);
  font-size: var(--z-text-small);
  cursor: pointer;
  min-block-size: var(--z-hit-min);
  transition: border-color var(--z-dur-fast) var(--z-ease-out),
              background-color var(--z-dur-fast) var(--z-ease-out);
}
/* §07 FORMAT CARD — SELECTABLE:
   "Selected = Churn fill + 2px Jam border + check. Unselected =
   Crème + hairline. Never a colored shadow." */
.check:hover { border-color: var(--z-jam); }
.check input { accent-color: var(--z-jam); inline-size: 1.1rem; block-size: 1.1rem; }
.check:has(input:checked) {
  border-color: var(--z-jam);
  background: var(--z-churn);
  font-weight: var(--z-weight-medium);
}

/* Off-screen rather than display:none — bots that check computed style
   for hidden inputs still fill this one in. */
.honeypot {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Centred and flush: one question per screen, so the controls sit
   directly under it rather than floating at the start edge. */
.fnav {
  gap: var(--z-space-3);
  align-items: center;
  justify-content: center;
  margin-block-start: 0;
}
.fnav [hidden] { display: none; }
/* Collapses entirely when there is no error, so it never contributes
   spacing above the nav. role="alert" still announces it when filled. */
.form__global-error:empty { display: none; }
.form__global-error {
  margin-block-end: var(--z-space-4);
  font-size: var(--z-text-small);
  font-weight: var(--z-weight-medium);
  color: var(--z-jam);
}
/* Submit spans the full measure of the form at every width. It is the last
   action in a nine-question funnel — it should read as the end of the form,
   not as one more inline control. The 768px rule that used to shrink it back
   to `auto` is deliberately gone. */
.form__submit {
  inline-size: 100%;
  margin-block-start: var(--z-space-6);
  padding-block: var(--z-space-4);
  font-size: var(--z-text-body);
}
.form--wizard .form__submit { margin-block-start: var(--z-space-6); margin-inline: 0; display: flex; }

/* ── budget slider ───────────────────────────────────────
   The control posts an index and form.js writes the band name into the
   <output>. Track and thumb are styled for both engines: WebKit needs the
   ::-webkit-slider-* pseudos, Firefox the ::-moz-range-* ones, and neither
   inherits from the other — a single rule set would silently style one
   browser only. */
.range { display: flex; flex-direction: column; gap: var(--z-space-3); }
.range input[type="range"] {
  inline-size: 100%;
  block-size: var(--z-hit-min);
  appearance: none;
  -webkit-appearance: none;
  background: none;
  cursor: pointer;
}
.range input[type="range"]:focus-visible { outline: 2px solid var(--z-focus-ring); outline-offset: 4px; }

.range input[type="range"]::-webkit-slider-runnable-track {
  block-size: 6px;
  border-radius: var(--z-radius-full);
  background: var(--z-chart-track);
}
.range input[type="range"]::-moz-range-track {
  block-size: 6px;
  border-radius: var(--z-radius-full);
  background: var(--z-chart-track);
}
.range input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  -webkit-appearance: none;
  inline-size: 26px;
  block-size: 26px;
  margin-block-start: -10px;   /* centres the thumb on the 6px track */
  border-radius: var(--z-radius-full);
  background: var(--z-gold);
  border: 3px solid var(--z-jam);
  transition: transform var(--z-dur-fast) var(--z-ease-out);
}
.range input[type="range"]::-moz-range-thumb {
  inline-size: 26px;
  block-size: 26px;
  border-radius: var(--z-radius-full);
  background: var(--z-gold);
  border: 3px solid var(--z-jam);
  transition: transform var(--z-dur-fast) var(--z-ease-out);
}
.range input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.12); }
.range input[type="range"]:active::-moz-range-thumb { transform: scale(1.12); }

.range__out {
  /* Was a solid Jam pill — identical shape, fill and radius to .btn--primary
     (border-radius: full, solid Jam background, Churn text). It read as a
     third clickable action next to Start a project / See the numbers, but
     an <output> does nothing on click. Every button in this system is a
     filled or outlined PILL (primary, gold, ghost); dropping the pill
     entirely — no fill, no radius, no border — is what makes it
     unmistakably a readout instead of a fourth button family.

     What replaces the pill is the same typographic weight this system
     already uses for a big number (.donut__value, .stat__value): bold
     display type, no container. Matches the slider's own inline-size, then
     backs off a step on each side — "as wide as the slider, yet a bit
     smaller" — and stays centered. */
  align-self: center;
  inline-size: 100%;
  max-inline-size: calc(100% - (var(--z-space-8) * 2));
  margin-inline: auto;
  box-sizing: border-box;
  text-align: center;
  color: var(--z-jam);
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-black);
  font-size: var(--z-text-h2);
  letter-spacing: var(--z-tracking-h1);
}
/* Untouched state. The prompt has to be clearly NOT an answer, so it drops
   out of the big display weight a chosen band gets and reads as an
   instruction instead.

   ONE state class, not two. `is-unset` ships in the markup so the pre-JS
   paint is already correct, and form.js toggles the same class — an earlier
   pass used a separate `--unset` modifier for the markup and a `:not()` rule
   for the chosen state, which inverted at exactly the wrong moment: the
   `:not()` was more specific and won before JS ever ran. */
.range__out.is-unset {
  font-family: var(--z-font-body);
  font-weight: 500;
  font-size: var(--z-text-small);
  letter-spacing: 0;
  color: var(--z-fg-faint);
}

.form__success {
  margin-block-start: var(--z-space-12);
  padding: var(--z-space-8);
  border: 2px solid var(--z-jam);
  border-radius: 2px;
  clip-path: var(--z-chamfer);
  background: var(--z-gold);
}
.form__success h3 {
  font-size: var(--z-text-h1);
  letter-spacing: var(--z-tracking-h1);
  margin-block-end: var(--z-space-3);
}

/* ── games hub ──────────────────────────────────────────── */
.game-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: var(--z-space-6);
  list-style: none;
  margin-block-start: var(--z-space-12);
}
/* One game, one card. auto-fit collapses the track to a single column and
   1fr then stretches it the full width of the container — a lone card
   running 1100px wide reads as a broken layout, not as a feature. Cap it at
   a comfortable reading width and CENTRE it, so the single card looks
   deliberate rather than like a row that lost its other items. Written with
   :has so all of this evaporates the moment a second game ships. */
.game-grid:has(> :only-child) {
  grid-template-columns: minmax(17rem, 30rem);
  justify-content: center;
}
/* A lone card is the hero of this page, not one tile in a row, so its
   contents centre too — left-aligned copy under a centred button was the
   part that read as unfinished. Only ever applies in the single-card case;
   a real row goes back to left-aligned, which is right for scanning. */
.game-grid:has(> :only-child) .game-card {
  align-items: center;
  text-align: center;
  padding-block: var(--z-space-8);
}
/* The badge rides in the corner when cards sit in a row. On a centred solo
   card that corner is a long way from the content, so it comes inline and
   sits above the art as a caption instead. */
.game-grid:has(> :only-child) .game-card__badge {
  position: static;
  align-self: center;
}
.game-grid:has(> :only-child) .game-card__art {
  inline-size: 4.5rem;
  block-size: 4.5rem;
}
.game-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--z-space-3);
  background: var(--z-paper);
}
.game-card__badge {
  position: absolute;
  inset-block-start: var(--z-space-4);
  inset-inline-end: var(--z-space-4);
  padding-block: var(--z-space-1);
  padding-inline: var(--z-space-3);
  border-radius: 2px;
    clip-path: var(--z-chamfer-sm);
  background: var(--z-gold);
  color: var(--z-jam);
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  text-transform: uppercase;
  letter-spacing: .1em;
}
.game-card__badge--soon { background: var(--z-chart-track); color: var(--z-fg-muted); }

/* Shape pass, Aug-2026: the card and its badge were a big soft rectangle
   and a 999px lozenge — the two shapes that read as generic. The button
   shape has since been chosen, so the provisional radii here are now
   re-derived from it as promised: both take the cut, the badge at the
   shallow depth because it is a small box. */
.game-card { border-radius: 2px; clip-path: var(--z-chamfer); }
.game-card__badge { border-radius: 2px; }
.game-card__art {
  inline-size: 3.5rem;
  block-size: 3.5rem;
  /* Fully Melt Gold. The sprite's default is Jam ink with a single gold
     accent; both recolour hooks are set here so the whole mark reads as
     one colour rather than a two-tone one. Safe to drop the Jam ink: the
     art is aria-hidden decoration sitting above a titled card, so nothing
     depends on it being legible on its own. */
  --icon-ink: var(--z-gold);
  --icon-accent: var(--z-gold);
}
/* Baloo 2 lives ONLY in the games segment — "the only place the voice
   gets loose" — and is always lowercase. */
.game-card__title {
  font-family: var(--z-font-accent);
  font-weight: 800;
  font-size: var(--z-text-h1);
  letter-spacing: 0;
  text-transform: lowercase;
}
.game-card__tagline { font-size: var(--z-text-small); color: var(--z-fg-muted); flex: 1; }
.game-card__meta {
  display: flex;
  gap: var(--z-space-2);
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  color: var(--z-fg-faint);
}
/* Pinned to the bottom of the card and centred. margin-block-start:auto is
   what makes the buttons line up ACROSS the row — the cards are equal height
   from the grid, but their copy is not, so without this each button floats
   at whatever height its own text ended. */
.game-card .btn { align-self: center; margin-block-start: auto; }
.game-card--soon { opacity: .8; }

.games-hub__sub { color: var(--z-fg-faint); }
.games-hub__note {
  margin-block-start: var(--z-space-12);
  padding-block-start: var(--z-space-6);
  border-block-start: 1px solid var(--z-hairline);
  font-size: var(--z-text-small);
  color: var(--z-fg-muted);
}

/* Legal line, not a sales line: smaller and quieter than .games-hub__note,
   and directly under it so the two read as one block rather than as a new
   section. No top border — the note above already draws one. */
.games-hub__legal {
  margin-block-start: var(--z-space-3);
  max-inline-size: 60ch;
  font-size: var(--z-text-tiny, .75rem);
  line-height: 1.7;
  color: var(--z-fg-muted);
  opacity: .8;
}

/* ── thanks ─────────────────────────────────────────────── */
.thanks { text-align: center; padding-block: var(--z-space-24); }
.thanks__mark { inline-size: 4rem; block-size: 4rem; margin: 0 auto var(--z-space-6); }
.thanks .section__title { margin-inline: auto; }
.thanks .lede { margin-inline: auto; margin-block-end: var(--z-space-8); }

/* ── footer ─────────────────────────────────────────────── */
/* No top margin: the gap exposed the Churn body behind it as a yellow
   band between the last section and the Jam footer. Sections carry
   their own padding, so the footer butts straight up against them. */
.site-footer {
  background: var(--z-jam);
  color: var(--z-white);
  padding-block: var(--z-space-16) var(--z-space-8);
  /* White on Jam. Set through the --z-fg* chain rather than a blanket
     `color`, because the footer's headings, links and legal line each read
     a different step of it — overriding only `color` would leave those
     three still pulling the old Churn tints. */
  --z-fg: var(--z-white);
  --z-fg-muted: var(--z-white-soft);
  --z-fg-faint: var(--z-white-faint);
  --z-hairline: var(--z-hairline-rev);
  --icon-light: var(--z-creme);
}
.site-footer__inner {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: var(--z-space-8);
}
.site-footer__wordmark {
  font-family: var(--z-font-display);
  font-weight: var(--z-weight-black);
  font-size: var(--z-text-h2);
  letter-spacing: var(--z-tracking-display);
  text-transform: uppercase;
  margin-block: var(--z-space-3) var(--z-space-1);
}
.site-footer__tagline {
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  letter-spacing: var(--z-tracking-label);
  text-transform: uppercase;
  color: var(--z-fg-muted);
}
.site-footer__col { display: flex; flex-direction: column; gap: var(--z-space-2); font-size: var(--z-text-small); }
.site-footer__title {
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  font-weight: var(--z-weight-medium);
  letter-spacing: var(--z-tracking-label);
  text-transform: uppercase;
  color: var(--z-fg-faint);
  margin-block-end: var(--z-space-2);
}
.site-footer__col a { color: var(--z-fg-muted); }
.site-footer__col a:hover { color: var(--z-gold); }
.site-footer__legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--z-space-3);
  margin-block-start: var(--z-space-12);
  padding-block-start: var(--z-space-6);
  border-block-start: 1px solid var(--z-hairline);
  font-family: var(--z-font-label);
  font-size: var(--z-text-label);
  color: var(--z-fg-faint);
}

/* ── client logo marquee ─────────────────────────────────
   Two identical tracks side by side, the pair sliding exactly -50% of the
   container. When the animation wraps, the second copy is sitting precisely
   where the first began, so the loop has no seam and no jump.

   direction:ltr is deliberate. Under RTL the flex row would reverse and the
   marquee would travel the other way on the Arabic page — logos are not
   language dependent, so the strip reads identically in both. */
.logos {
  display: flex;
  gap: var(--z-space-16);
  inline-size: 100%;
  overflow: hidden;
  direction: ltr;
  margin-block-start: var(--z-space-12);
  /* A mask, not a gradient overlay: the strip has to fade into whatever
     surface the section is using, and an overlay would have to hard-code
     that colour. SHADOW is a design-system DON'T; a mask is neither. */
  /* currentColor, not a literal: a mask only reads the ALPHA channel, so any
     opaque value works — and currentColor keeps this inside the token law
     instead of smuggling a hex into main.css. */
  -webkit-mask-image: linear-gradient(to right, transparent, currentColor 6%, currentColor 94%, transparent);
          mask-image: linear-gradient(to right, transparent, currentColor 6%, currentColor 94%, transparent);
}
.logos__track {
  display: flex;
  align-items: center;
  gap: var(--z-space-16);
  flex-shrink: 0;
  list-style: none;
  min-inline-size: max-content;
  animation: logoMarquee var(--z-dur-marquee) linear infinite;
  /* Promote to its own compositor layer. The strip is full-bleed and sits
     under a mask, and a masked region that changes has to be re-composited
     every frame — without this the browser repaints the whole band 60
     times a second instead of just moving an existing layer, which is what
     made a 31px/second crawl stutter. transform-only animation on its own
     layer is the one path that stays off the main thread. */
  will-change: transform;
  backface-visibility: hidden;
}
.logos:hover .logos__track,
.logos:focus-within .logos__track { animation-play-state: paused; }

.logos__item { display: grid; place-items: center; }
.logos__item img {
  /* Capped on BOTH axes so a 7:1 wordmark and a 1:1 disc carry the same
     optical weight. Height alone would make VogaCloset three times the
     presence of Visit Saudi. */
  max-block-size: 2.75rem;
  max-inline-size: 9.5rem;
  inline-size: auto;
  block-size: auto;
  object-fit: contain;
  /* The marks are full colour by necessity — a brand owns its palette — so
     they are held back a little and come up on hover, which keeps the strip
     from competing with the section heading. */
  opacity: .78;
  transition: opacity var(--z-dur-base) var(--z-ease-out);
}
.logos:hover .logos__item img { opacity: 1; }

/* Visit Saudi and Yousef Afandi are round marks, not wordmarks — at the
   same cap as the text-based logos they read small, because a circle
   fills its box far less densely than a wordmark does. Sized up so their
   visual weight matches the row instead of their literal box.
   Placed AFTER .logos__item img on purpose: equal specificity (0,2,0),
   so cascade order decides, and this must come second to win. */
.logos__item--mark img { max-block-size: 4.5rem; max-inline-size: 4.5rem; }

@keyframes logoMarquee { to { transform: translateX(calc(-100% - var(--z-space-16))); } }

/* Motion is decoration here; the logos are the content. Under reduced
   motion the strip stops and wraps into a plain centred row, and the
   duplicate track is removed so nothing is announced or shown twice. */
@media (prefers-reduced-motion: reduce) {
  .logos { flex-wrap: wrap; justify-content: center; overflow: visible;
           -webkit-mask-image: none; mask-image: none; }
  .logos__track { animation: none; flex-wrap: wrap; justify-content: center; }
  .logos__track[aria-hidden="true"] { display: none; }
}
