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.
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.
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.
Placeholder copy
The Championship
Seven weeks. One board. Placeholder text standing in for the client's own.
Pointed at the real close, 19 July 2026. It reads zero because the clamp holds it there.
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.
- 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
- 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.
- 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
- 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.