Collins TM


Post 21 · May 22nd, 2026

The Summer Whoop Challenge

Dinner on the line for this challenge.

Cecil, Jaisen, TJ, and I are running a fitness challenge through the summer. The prize is simple: the winner gets an all-inclusive dinner at The Precinct steakhouse in Cincinnati while everyone is in town for ATP Cincinnati in August — paid for by the three losers. Stakes are real.

The scoring pulls from three Whoop metrics each day: Strain, Recovery, and Sleep. Each day the four of us are ranked 1–4 across those categories. The points stack daily. Whoever has the most at the end of summer eats for free. The three losers picks up the whole tab, Kobe A5 here I come.

Five days in. Here's where we stand.


Standings — May 22, 2026

Leading
#1Collins 34 pts
Avg Strain 10.4
Avg Recovery 82%
Avg Sleep 90%
#2Cecil 24 pts
Avg Strain 12.0
Avg Recovery 58%
Avg Sleep 68%
#3TJ 21 pts
Avg Strain 11.1
Avg Recovery 46%
Avg Sleep 77%
#4Jaisen 5 pts
Avg Strain 5.6
Avg Recovery 36%
Avg Sleep 45%

I'm in front right now, but it's early. Cecil is pushing hard on Strain — he's averaging the highest of anyone at 12.0. TJ is consistent. Jaisen had a rough start to the challenge but he's the one who gave me the one-Amazon-order-a-week rule, so I'm not writing him off. The gap is real today. It won't necessarily stay that way.


Daily Points Log

Date Cecil Collins Jaisen TJ
May 18
May 19
May 20
May 21
May 22

The Code: How the Scoreboard Works

The scoreboard above uses nothing but HTML and CSS — no JavaScript, no charts library. Here's what makes each piece work.

The Points Bar

Each player card has a thin horizontal bar that shows their score relative to the leader. The bar is built with two nested divs: an outer track and an inner fill. The fill's width is set inline using a calc() expression that does the math directly in the markup.

<div class="pts-bar-track">
  <div class="pts-bar-fill"
       style="width: calc(24 / 34 * 100%)">
  </div>
</div>

The track gets overflow: hidden so the fill can never escape its container. The border-radius on both elements keeps the pill shape consistent even as the fill width changes.

.pts-bar-track {
  width: 100%;
  height: 5px;
  background: #e7e5e4;
  border-radius: 3px;
  overflow: hidden;
}

.pts-bar-fill {
  height: 100%;
  background: #1c1917;
  border-radius: 3px;
}

The Leader Highlight

The leader card gets a darker border and a small "Leading" tag that drops from the top right corner. This is done with a single modifier class on the card and a position: absolute badge inside it. The badge is hidden by default with display: none and only shown when the parent has the .leader class.

.player-card.leader {
  border-color: #1c1917;
}

.leader-tag {
  display: none;            /* hidden on all cards by default */
  position: absolute;
  top: -1px;
  right: 14px;
}

.player-card.leader .leader-tag {
  display: block;           /* visible only on the leader */
}

The Mini Stat Bars

Each card has three smaller bars for Strain, Recovery, and Sleep. They work the same way as the points bar — a track with an overflowing fill — just at 3px height instead of 5px. Each metric gets its own fill color class so they stay visually distinct without needing inline styles for color.

.fill-strain   { background: #44403c; }
.fill-recovery { background: #57534e; }
.fill-sleep    { background: #78716c; }

The three stat blocks sit in a CSS Grid with grid-template-columns: repeat(3, 1fr), which divides the available width into three equal columns automatically regardless of the card width.

The Dot Log

The daily points table uses a row of small div circles instead of raw numbers. Each dot is 7px wide, 7px tall, and fully rounded. Filled dots are full opacity. Empty dots sit at 15% opacity — present but quiet. This lets you scan the week visually rather than reading integers.

.dot {
  width: 7px; height: 7px;
  border-radius: 50%;
  background: #1c1917;
  opacity: 0.15;          /* empty state */
}

.dot.filled {
  opacity: 1;             /* active state */
}

The dots share the same HTML element. The only difference between filled and empty is that single class toggle. Simple system. Easy to update as scores come in.