← All Finish the Line pages

Finish the Line — the design system

A screen system for a timed phone game. Three routes, ten states, four owned components, roughly ninety tokens. This describes what ships on finishtheline.cc as of 9 August 2026, not what it should be — where a claim differs from the running app, it says so.

The product in one line: three lines of dialogue, two of them fakes, and sixty seconds to keep picking the real one.

Sources

Authored by reading the running code, in order of authority: the app's own stylesheet (which wins every disagreement), its markup and states, its runtime-built pieces (option rows, result cells, the share label), then the design system's token files, copied verbatim.

Verified. Every token value here is copied from the source, not retyped from memory. The contrast table below computes its ratios from rendered colour at view time, rather than printing numbers written into a comment — that distinction has already caught a token shipping at 3.17:1 under a comment claiming it passed, and a contrast figure recorded elsewhere at 2.4× the truth.

Colour

Four state hues, one job each — sage means correct and only correct, brick means wrong, gold means the clock is short, slate means the clock ran out. Azure is at-rest and focus. Every hue is one value in both modes, with a single white ink over all of them.

Four hues, one job each

Correct — only ever this
#41763f · 5.40 on white
Wrong · clock nearly gone
#b5453a · 5.42
!
Clock short
#806c1f · 5.14
The clock ran out on it
#6b6e70 · 5.14

⛔ A state hue is always a fill or a border, never text. All four sit at roughly 0.13–0.15 luminance, which is why one white ink works over every one of them in both modes — and why they cannot be text.

⚠ The cost, stated plainly. Adjacent hues separate by only 1.05–1.40:1, told apart by hue alone — which a colour-blind player cannot rely on. That is acceptable here only because every state also carries a ✓ / ✗ / – glyph, shown in the swatches above. The glyph is the accessibility mechanism, not decoration. An answered option or a result cell must never render without one.

⛔ Green is never the brand. The mark used to share the correct-answer green with the wordmark — one colour meaning both "Finish the Line" and "you got it right", thirty seconds apart. It was moved to ink instead. Both cleared contrast, so this was a meaning fix, not a contrast fix.

Surfaces do not separate — borders do

The two page surfaces are almost the same colour: raised clears the page by only 1.06:1 in light and 1.09:1 in dark — far below the 3:1 a non-text boundary needs. A fill can never be what makes something a distinct, tappable object here; the border is.

Light page

Fill only — where does this row end?
Border — now it is an object

Dark page — what ships

Fill only — where does this row end?
Border — now it is an object

The three tint tokens (--ftl-tint-hit / -miss / -idle) measure about 1.1:1 against the page by design — they carry the hue quietly. The full-strength border carries the contrast, at 3.44–3.45:1. They are opaque mixes with the page, never translucent overlays, so the value measured is the value painted.

Contrast, measured in your browser right now

Every ratio below is read back out of the rendered page, not typed into this file. Pairs are enumerated from the components, not the token list — a token-list audit passes every pair it thinks of and misses the ones components actually compose.

PairWhere it appearsRatioAA

The one row that fails is deliberate, and it is here to be looked at. ink-subtle on surface-sunken measures 4.35:1 in light and passes at 4.65 in dark — a pairing that is fine in the mode the product ships and fails in the mode it also supports. The row below it is the same badge on ink-muted — this system's component CSS already uses that fix; the running app does not yet.

Type

One content rule decides the family, every time: the serif is worn by anything a character actually said; the sans by everything the app says about them. Under a clock, a player comparing three options is comparing quotes with quotes, and the serif says which text is in the game before a word of it is read.

The lines below are placeholder text written for this page. No dialogue from the show appears anywhere in this design system.

Reading sizes — all three ship

13px · label--ftl-text-xs Warm-up · the clock isn't running yet
17px · narration--ftl-text-md Guess wrong and you lose 5 seconds off the clock.
19px · dialogue--ftl-text-lg "The third one is the one they said."

Display numerals — exempt, because they are glanced at, not read

32px · the score--ftl-display-sm 7 lines in 60 seconds
44px · the clock--ftl-display-md 60.0
64px · the countdown--ftl-display-lg 3

Every numeral above is tabular — the clock counts down in tenths, so with proportional figures the whole line reflows ten times a second while the player is trying to read three sentences against it. 15px and 22px exist in the token file and no shipped page uses either; they are not deprecated, just unclaimed.

⛔ Never set a UI string in the serif to make it feel special. Every serif string on screen is a claim that a character said it.

⛔ Only three faces ship — Inter 400, Inter 600, Newsreader 400. There is no Newsreader italic and no Inter 700. Asking for one renders a synthesised face — the actual bug behind one option row painting faux-bold, because Jelly's own vendored radio sets weight 600 and only a 400 serif was loaded.

Layout

On the round screen a player must see the clock, the cue and all three options without scrolling. Telegram's in-app browser on a 375×667 phone leaves roughly 560px. That is the whole budget, and two tokens spend it: --ftl-budget-timer (≤96px) and --ftl-budget-question (≤440px).

⛔ The rule this forces: anything that appears AFTER an answer is anchored to the viewport, never appended to the flow. Appending pushes the explanation off the bottom of the screen on exactly the long questions where the player most needs it — which is why the reveal is a fixed sheet, capped at 42% of the dynamic viewport.

Measured at 375px: an option row is 56px to about 39 characters, 79px to about 77, and 108px beyond that — so three rows alone can cost 180 to 336px before a word of scene text. That is the real argument for keeping candidate lines short.

44px is a floor, and it has two axes

The measurement that settled this: a vendored radio control paints 41.8px tall and about 210px wide inside a 335px column — under the floor on one axis and, on the other, narrower than the row it sits in, so taps at the right-hand end land on nothing.

41.8 × 210

Hatched = looks tappable, is not.

Full-bleed 56px row — the row is the target

Assert 44, set 52. A min-height floor that lands exactly on 44 leaves nothing for a rounding error or a font swap. FTL cannot go fully square either: the vendored controls paint canvas-based soft-body surfaces, and a 0px corner beside one reads as a rendering bug — so elements FTL owns go sharp (4px chips, 8px rows, pill tracks) and the vendored controls keep their soft geometry.

Motion

In a 60-second game, animation time is round time. A staggered per-element reveal of the kind daily word games use runs about 1.7 seconds per tile; at 8–12 questions that is 14 to 20 seconds of a 60-second round spent watching an animation. So the reveal is one state change, 120ms, simultaneous across all three options.

Three durations exist and no more: 120ms (state changes), 200ms (the sheet sliding in, screen fades), 400ms (unclaimed). All three collapse to 1ms under prefers-reduced-motion — collapse, not disable, so nothing waiting on a transition event stops firing.

Components

Four things the app owns outright, in plain CSS on plain elements. The candidate lines below are placeholder text; no dialogue from the show appears anywhere in this system.

Option row — every state

1"The third one is the one they said."
2"The third one is what they said."
3"The third one is the thing they said."

Fill decorative, border carries the state, glyph carries the meaning. The player's own pick is the filled row; the correct row (when they picked wrong) is outline-only, so it does not compete with their own filled pick.

Clock — four phases

41.8

At rest

12.4

Short — under 15s

3.1

Critical — grows too

The bar grows as well as reddens at critical, because it is read peripherally — the player is looking at the option rows, not the clock. Frozen (reading an answer) dims the number rather than hiding it, so the player keeps a continuity cue that this is the same question.

Result grid — the thing people screenshot

⚠ Known gap: eight cells orphan one on its own line. A 335px column holds 7 cells at 40px with an 8px gap; an eight-answer round wraps to two rows with a single cell alone on the second — which reads as a mistake rather than a wrap. Nine or ten answers look fine; eight is the ugly case. Not fixed — flagged for a decision between accepting the wrap, shrinking the cell, or capping the row so it always breaks evenly.

The score is a plain count of lines, not a named tier — tiers used to draw on show-specific in-jokes that read as nonsense to a first-time player, on the one screen most likely to be screenshotted and sent to someone who has never played.

Vendored controls

Everything else on screen is a vendored web component (a button, a radio, a progress bar, a spinner) that paints to a canvas inside a shadow root. That single fact decides everything below.

Custom properties cross the shadow boundary. Selectors do not. The only way to restyle one of these is to set the property its painter actually reads, on the host — not ::part(), not a descendant selector, not !important on a class.

⛔ Never bind one of these tokens to color-mix() or oklch(). These components hand the raw computed colour string straight to canvas with no resolver. An invalid fill value is silently ignored and the previous colour simply persists — the control keeps painting, in the wrong colour, with no error anywhere.

⛔ Every one of these controls bleeds about 44px past its own host, on every side, unconditionally. It is a soft-body wobble margin, not a stale-paint bug — the bleed has to be contained with overflow-x on both html and body, since an HTML document propagates whichever one of them declares a value to the other.

Anything the app owns outright — the option row, the result cell, the reveal sheet, the type roles — is plain CSS on plain elements, and none of the above applies to it. The vendored controls are a boundary, not a foundation.

These frames are drawn with plain CSS throughout this page and the pages kit — loading the real vendored bundle here would make every specimen depend on a remote script.

Voice

Dry sitcom beat. No emoji, no exclamation marks, no hype — in the interface. The game is already funny; the interface does not need to be. Its job is to be out of the way in under a second.

Every string the player reads — the complete set

/ · pitchThree lines. Two are fakes. Pick the one they really said.
/ · pitchA daily game built from Friends.
/ · pitchThe 60-second round opens 10 September.
/quiz · landingThree Friends lines. Two are fakes. Pick the one they really said.
/quiz · landingSixty seconds. The clock stops while you read the answer.
/quiz · landingGuess wrong and you lose 5 seconds off the clock.
/quiz · landingRoss has the clipboard. The girls are already losing.
/quiz · warm-upWarm-up · the clock isn't running yet
/quiz · countdownLightning round — Ready…
/quiz · revealKeep going
/quiz · results7 lines in 60 seconds
/quiz · resultsPlay again
/tvOpens 10 September

Do

  • State the rule, then stop. "Guess wrong and you lose 5 seconds."
  • Let the interface carry the verdict — the row already went green
  • Keep the one in-world joke per screen, at most
  • Grade 8–10 reading level, hardest on anything a first-timer sees

Never

  • Emoji or exclamation marks anywhere in the product
  • Restating the answer in words — the reveal sheet never repeats it
  • A joke only someone deep in the show can parse
  • Praise for a correct answer. It was one line, in a 60-second round

The score tiers were deleted. They named results in show-specific in-jokes that read as nonsense to anyone not deep in the series — on the one screen most likely to be screenshotted and sent to someone who has never played. The standing test for any new string: would a first-time player know what this means?

The reveal never restates the answer — the correct row already outlines and pops; saying it again in words is the same answer twice.

The shared text

Three lines of plain text. No stylesheet, no fonts, no tokens — whatever the receiving chat client decides. It travels further than the site and is harder to withdraw than anything else here, so it gets its own rules.

🥄 Finish the Line — 7 lines in 60 seconds ✅✅❌✅⬜✅❌✅ https://finishtheline.cc

✅ ❌ ⬜, never 🟩 🟥. Green-versus-red is the one pair red-green colourblindness cannot separate — and because emoji hue is decided by the renderer, there is no contrast fix to reach for. Shape has to carry it.

No episode names, ever. A result posted into a group chat must not spoil a line for whoever reads it — the grid is abstract rather than a list of what you got, and that is a content rule, not a space saving.

Emoji are allowed here, and nowhere else. The interface bans them because the app controls its own rendering and has real glyphs, colour and type to work with. This message has none of that — the rule is about capability, not taste.

This project's posture is fan-scale, private and takedown-compliant. A logo is the single most legally exposed thing it can ship, because a mark is exactly what a trademark protects. Evoking an era is fine; quoting the show is not.

Never

  • A dot-separated wordmark lettering out the show's name
  • Individually coloured dots between letters — in a logo, a button, or anywhere else
  • The orange sofa, the umbrella, the frame-in-a-frame
  • Character names or catchphrases as brand elements
  • Dialogue from the show on any shared URL, share card or preview image

Fine

  • Saying plainly that the game is built from the show
  • Period-adjacent type and colour that quote nothing specific
  • Marks built from the game's own mechanic — lines, quotes, the clock
  • The show's name in running prose, where it is a factual description

⚠ One pattern above is currently live on the product and is under review by the owner for exactly this reason — it is not reproduced here, on purpose, since showing it would defeat the point of flagging it. See the logo concepts page for the replacement under consideration.

The share card is the sharpest edge. An og:image is scraped, cached and re-hosted by every chat client that touches the link, so it travels further than the site and is harder to withdraw than anything else the project publishes. Nothing from the question corpus may appear in it.

Brand

Four logo concepts are still on the table — see the logo concepts page for all four at real 16px in a real browser tab strip, in dark and light. No mark has been chosen yet.