Skip to content
KR.

Warren Bowie & Smith · 200Invest

2026

Traders Cup

A seven-week trading tournament, built twice — once for three Latin locales, once for Arabic with the whole page mirrored.

Design · Front-end · MotionOffline

Shipped

Twice

Warren Bowie & Smith — Champions Cup · 200Invest — Traders Championship

Framework

Astro 4, static output

Styling: Vanilla CSS, no utility framework

Techniques

4

11 constants, formulas and guards

Promotional sites for two trading tournaments running the same seven weeks of 2026, each built as a single scrolling page that behaves like a competition control room: a live countdown to the close, a podium of prizes, a standings board that polls while you read, and the mechanics of scoring laid out plainly enough to survive a compliance review. Both were designed to mount at a sub-path of an existing brand site rather than as standalone domains.

Offline

Both campaigns ran to a fixed close on 19 July 2026 and came down with it. wbandsmith.com/traders-cup-2026/ and 200invest.com/traders-championship-2026/ both return 404 today, checked rather than assumed. Nothing on this page can be verified against a live page, and the page does not pretend otherwise.

The same seven components in both builds, and not one shared file between them.

Latin buildMENA build
ControlsToolbar
FAQ
HeroPanel
HowItWorks
LeaderboardCard
PrizeCards
StickyCtaBar
The same seven component names in both repositories, and not one shared file. Bars are byte sizes: PrizeCards is five times larger in the MENA build, HeroPanel four. Same decomposition, independently rewritten.

Built once, shipped twice.

  • Warren Bowie & Smith — Champions Cup

    Latin America and Europe

    en / es / pt

    A three-stage tournament, so the standings board carries Starters / Contenders / Champions as tabs and the reader picks a bracket. Poppins and JetBrains Mono, countdown to 19 July 23:59 UTC, and a magnetic hero CTA the second build has no equivalent of.

  • 200Invest — Traders Championship

    MENA

    en / ar

    One board rather than three, and the entire document mirrors: `dir` flips to rtl on the Arabic route and the font stack swaps with it. Countdown to 19 July 23:59 GST, a Sharia-compliance FAQ, and a fixed eighteen-clause terms page the first build does not have.

The mechanic, rebuilt and running.

Rebuilt from the constants quoted below, on this page, in your browser. It is not the client’s board — none of their artwork, copy or offer is here. What is reproduced is the part that was mine to reproduce.

Magnetic CTA active — hover it

Placeholder copy

The Championship

Seven weeks. One board. Placeholder text standing in for the client's own.

Total prize pool$25,000
Entries closed
00D
00H
00M
00S

Pointed at the real close, 19 July 2026. It reads zero because the clamp holds it there.

RankTraderPoints
01Trader 041718,420
02Trader 118017,655
03Trader 009216,980
04Trader 224415,310
05Trader 073114,875

Placeholder copy and invented standings throughout. Neither client’s text, type, prize structure or data is reproduced here — what is rebuilt is the layout and the three pieces of motion this study makes claims about.

Each one starts with something that was broken.

  1. 01

    Mirroring is a document decision, not a stylesheet patch

    The failure

    Arabic support is usually retrofitted: ship the Latin page, then chase every left-aligned rule with an `[dir=rtl]` override. The overrides multiply, the two directions drift, and the version nobody speaks fluently is the one that breaks.

    The decision

    Set direction once, on the document, from the route's own locale, and let the font stack follow it in the same expression. Everything downstream inherits — so the mirrored build is the same components reading the same JSON, not a second stylesheet maintained in parallel.

    • constantsrc/layouts/BaseLayout.astro
      const dir = locale === "ar" ? "rtl" : "ltr"
    • constantsrc/layouts/BaseLayout.astro
      <html lang={locale} dir={dir}>
    • guardsrc/layouts/BaseLayout.astro
      fontHref — IBM Plex Sans Arabic + Inter on ar, Inter + Sora otherwise
  2. 02

    The numbers stay Latin even when the page does not

    The failure

    A prize figure animating from zero is the page's one moment of theatre, and it is the easiest place to lose a reader: locale-aware formatting will happily render Eastern Arabic numerals, which is correct by the letter and wrong for an audience that reads trading figures in Latin digits all day.

    The decision

    Keep the counter locale-aware for grouping and separators and hold the digits Latin, in both builds. The first parses the target and its formatting out of the rendered text; the second — written after it — moves the contract onto data attributes so the markup declares its own prefix, suffix and locale instead of having them inferred.

    • constantsrc/scripts/count-up.js
      DURATION = 1200, EASE_OUT = 1 − (1 − t)³
    • constantsrc/scripts/prize-count.js
      duration = 1400, eased = 1 − Math.pow(1 − t, 3)
    • guardsrc/scripts/count-up.js
      Renders 0 immediately on arm, so the page never flashes the final value first

    What it cost

    The two counters are genuinely different modules rather than one shared one, which is duplication. It bought the second build a formatting contract that lives in markup instead of in a regular expression, and on balance that was worth a file.

  3. 03

    The magnet knows when to switch itself off

    The failure

    A cursor-following CTA is the kind of flourish that reads as expensive on a laptop and as a bug everywhere else: on a touch device there is no cursor to follow, and on a narrow viewport a button that drifts is a button that is harder to hit.

    The decision

    Three gates before a single listener is bound — motion preference, pointer capability, viewport width — and a lerp that parks itself when it settles rather than holding a frame loop open for a stationary button. The offset is published as two custom properties, so CSS keeps ownership of the hover lift and the two never fight.

    • constantsrc/scripts/magnetic.js
      PULL = 0.22 · MAX_OFFSET = 8px · EASE = 0.22 per frame
    • guardsrc/scripts/magnetic.js
      prefers-reduced-motion: reduce ‖ (hover: none) ‖ innerWidth < 720 — never binds
    • guardsrc/scripts/magnetic.js
      rAF released once |target − current| < 0.1px
  4. 04

    Two clocks, because a deadline is local

    The failure

    Both tournaments close at the same instant, and writing that instant as one timezone would be wrong for one of the two audiences. A competitor should not have to convert their own deadline.

    The decision

    Each build states its close in the timezone its audience lives in — UTC for the Latin markets, GST for MENA — and every countdown on the page is driven from a `data-target` on the element rather than a constant in the script, so the hero clock and the topbar clock cannot drift apart.

    • constantsrc/scripts/countdown.js
      document.querySelectorAll('[data-countdown]') — every clock ticks, not just the first
    • constantsrc/scripts/countdown.js
      diff = Math.max(0, target − now) — a passed deadline reads 00:00:00:00, never negative

Every animation, with what it does when asked for less.

The last column is the one worth reading. “Respects reduced motion” is easy to assert and easy to skip, so where it is not true here, it says so.

Prize count-up

1200ms (Latin build) · 1400ms (MENA build)

Cubic ease-out — 1 − (1 − t)³

Skipped entirely. The final value renders immediately; the MENA build goes straight to its settled shimmer state.

Magnetic hero CTA

Continuous while the pointer is over it, released when settled

Linear interpolation, 0.22 of the remaining distance per frame

Never bound. Also skipped on touch pointers and below 720px.

Countdown tick

1s

None — a clock that eases is a clock that lies

Unaffected. It is information, not decoration.

Standings poll

30s

Rows re-render in place

Unaffected — no transition is applied to the swap.

What it was built on, what is claimed, and what is not.

Framework

Astro 4, static output

Both ship zero JavaScript by default and opt in per script. For a page whose only interactivity is a clock, an accordion and a poll, a client framework would have shipped a runtime to do what four small modules already do.

Styling

Vanilla CSS, no utility framework

Deliberate on a page that had to mirror. RTL in hand-written CSS is logical properties and a handful of overrides; RTL through a utility framework's directional class names is a second set of class names on every element.

Copy

One JSON file per locale

No string is written into markup in either build. It is what makes a third locale a translation job rather than a development job — and the reason the second build could add Arabic without reopening a component.

Type

Per-locale font loading

The Arabic route requests IBM Plex Sans Arabic with Inter; the Latin routes request Inter with Sora and never download the Arabic subset. One conditional, and the LTR pages stop paying for a script they cannot render.

Standings

Polled static JSON

A board that updates without a backend, a socket, or a request that can fail visibly. If the feed is unreachable the last-rendered board simply stays — the page degrades to stale rather than to empty.

Claimed — and how to check it

  • The two builds share no source file

    Compare git blob SHAs under src/components/ and src/scripts/ in both repositories — no pair matches.

  • The same seven-component decomposition appears in both

    Both repositories contain ControlsToolbar, FAQ, HeroPanel, HowItWorks, LeaderboardCard, PrizeCards and StickyCtaBar.

  • Full RTL on the Arabic route

    BaseLayout.astro derives `dir` from the locale and sets it on the html element.

  • No string is written into markup

    src/data/copy.*.json carries the copy for every locale in both builds.

  • The motion gates on reduced-motion preference

    magnetic.js returns before binding; count-up.js and prize-count.js both branch to the final value.

  • Both campaign URLs are down

    Request either URL — both return 404.

Not claimed

  • No sign-up count, participation figure, or deposit total. Both campaigns closed on 19 July 2026 and the numbers live in the clients' systems.
  • No claim that these are one design system. They share an architecture and no code, and the blob comparison is on this page precisely so nobody has to take the weaker claim on trust.
  • No claim over the tournament mechanics, the prize ladders, or the compliance copy. Those were the clients' and were built to, not designed here.
  • No accessibility audit. The reduced-motion gates are checkable in source; nothing here asserts a WCAG conformance level that was never formally tested.
  • The stages on this page are a rebuild of the layout and the motion, not the shipped site. No client copy, standings data, or artwork is reproduced.

Every constant is read out of the two repositories at the file named beside it. The not-shared claim is a comparison of git blob SHAs across both repositories under src/components/ and src/scripts/ — no path matches. The offline status was checked by request: both campaign URLs returned 404.