Status: SPEC. Nothing here is built. No code is scaffolded by this document.
Author date: 2026-08-08
Owns: the public face of the show. Does not own the office, the broadcast, or the releases.
Deployment: Cloudflare — after broadcast testing. Chairman's sequencing, see §7.
Finessionals is a 13-week international competition and broadcast: 65 weekday episodes
from Mon 12 Oct 2026 to Fri 8 Jan 2027, plus three Saturday streams (Elimination 1
on 7 Nov 2026, Elimination 2 on 5 Dec 2026, the Finale on 9 Jan 2027). Twelve people
compete for four seats across four tracks; the field narrows 3 → 2 → 1 per track.
Today the show exists in three places and none of them is public:
| Surface | What it is | Audience |
|---|---|---|
apps/office/public/finessionals/ | the office hub — ownership split, runbooks, setup sheets | operators, prospective hires |
governance/*.json | roles, pay, policy, pipelines — the show as data | the build |
engine/lib/season.py | the schedule — 65 episodes, generated | every renderer |
The office hub is written to be shared with a prospective Director. It is an internal
document that happens to be reachable. It assumes the reader already has context, it links
to /calendar, /harness, /reports and other operator surfaces, and it discusses
audio-bus discipline and fault-injection drills. That is the correct document for that
reader and the wrong document for the public.
The public site answers four questions the office does not:
And one it must answer without repeating anyone: what are the terms? Roles, the pay
ladder and the rules live in governance/. The site renders from those files or links to
the office page that already renders from them. It never restates a figure. Two copies of a
pay number become two different pay numbers, and a disagreement about pay is a dispute with
a real person about their income.
The site is a VIEW. It is not a second source of truth.
Every fact on the public site traces to engine/lib/season.py, governance/*.json, or a
human decision recorded in this file's §6 as still-open. If a page needs a fact that lives
in neither, that is a gap in the data — fix the data, not the page.
/ The show — what it is, when it starts, where to watch
/episodes All 65, browsable. Filter by week / block / track / brand
/episodes/<n> One episode: date, block, who leads, which brand, what happens
/watch Platforms, aspects, joint-stream policy, notify-me
/programme What the competition is: tracks, rounds, how the field narrows
/programme/roles Four seats + the Chairman — rendered from governance/roles.json
/programme/pay The ladder — rendered from governance/pay.json
/programme/rules The terms — rendered from governance/policy.json
/apply Intake form. International by design
/shop Merch. Hosted commerce, off-site checkout
/privacy What we collect, why, for how long, how to get it deleted
/terms Site terms. Distinct from participant terms
Thirteen routes. No blog, no news, no press page, no "about the team" until there is a team.
A route that would ship empty does not ship — see §5 on honest empty states.
Navigation: one bar, five items — Episodes · Watch · Programme · Shop · Apply.
/privacy and /terms live in the footer. /programme/* are children reached from
/programme, not top-level.
No dark mode. This is not a style preference. See §4.9.
/ — the showPurpose: a cold visitor understands what Finessionals is inside fifteen seconds, and
knows the date.
Blocks, in order:
Written once, here; every other page inherits it. Draft: *"Four seats. Twelve people.
Sixty-five episodes of real work on real brands, live, graded on air."*
season: a countdown to Episode 1. During: "Airing now — Week N, Episode n" derived from
the current date against season.episodes(). After: "Season one is complete."
/watch. Not a wall of embeds.roles.json[].one_line. Links to /programme/roles.
season.py. Pre-season thisblock shows week 1 with a "starts in N days" label — it is a schedule, not a claim that
anything has aired.
Must not contain: view counts, follower counts, "as seen on", testimonials, or any
number that is not derivable from the season structure. Before the season starts there is
nothing to count.
/episodes — all 65, browsableThis page is generated. It is not authored. The generator iterates
season.episodes() and emits one card per yielded dict. There is no hand-written list of
episodes anywhere in the site source, and adding an episode to the season must require
editing exactly one file: engine/lib/season.py.
Each yielded dict carries: `n, week, sprint, round, round_theme, seats, week_title,
week_desc, date, day, block, led_by, live, edit, brand, brand_label, em`. The page uses
n, week, date, day, block, led_by, brand_label, round and groups by week with
week_title / week_desc as the group header.
Layout: thirteen week sections, five episodes each. Each week header carries the week
number, week_title, week_desc, the round label, and the seat count still in play
(seats × 4 tracks). The three Saturday streams from season.saturdays() are interleaved
after weeks 4, 8 and 13 as visually distinct full-width rows — they are not weekday
episodes and must not be counted as such.
Episode card: episode number, date, weekday, block badge, who leads it, brand label.
Nothing else — the detail belongs on /episodes/<n>.
Block badges. Eight blocks, with counts that fall out of the spine:
| Block | Count | Who leads | One-line public gloss |
|---|---|---|---|
| BRIEF | 7 | Executive Manager | The week's scope, agreed on camera and written down |
| BUILD | 13 | Technical Operator | Terminal only. The unglamorous middle, shown rather than skipped |
| CROSS | 13 | counterparts | People doing each other's jobs, badly, in good faith |
| GATE | 7 | Technical Operator | The gates run live. Failures on screen, in red |
| CHECKPOINT | 7 | Producer | What is actually done versus what was claimed on Monday |
| ADJUST | 6 | Executive Manager | What the checkpoint changed, and what got cut |
| PUBLISH | 6 | Technical Operator | It ships or it does not. Every item traces to a registry entry |
| DEMO | 6 | each participant | Everyone shows what they built, live |
Those glosses are the only new copy on this page and they belong in the generator as a
BLOCK_GLOSS dict keyed by block name — eight strings, not sixty-five. They are derived
from the live text already in season.py's spine and must not contradict it.
Filters: week, block, brand, round. Client-side, no server. Every filter state must be
addressable by URL (/episodes?block=GATE) so an episode set can be linked to.
Honest empty state — this is the important one. Pre-season, every card shows a scheduled
date and no watch link, no duration, no VOD, and no clip count. The card
label reads "Scheduled", not "Coming soon" and never "0 views". A card only gains a
watch link once a real URL exists for it. See §5.
/episodes/<n> — one episode65 static pages, generated from the same iterator. Contents:
led_by).brand_label) — one of *Temerarii Media, Lil Shop of Culture,Dom Davis Network, A new brand, All brands*. All 65 episodes carry a brand; there is no
unbranded episode to handle.
live string from the spine/detail. This is the public-facingdescription and it is already written. Do not rewrite it on the site.
The edit field must NOT be published. It is production planning — *"highest-yield
clips of the week", "tension clips", "cut tight, no commentary"*. Telling the audience which
moments have been pre-designated as the tense ones changes what those moments are. It stays
internal. This is a filter the generator applies deliberately, and it should carry a comment
saying so, because the field is right there in the dict and the omission looks like an
oversight otherwise.
The em field must NOT be published until §6 decision D6 resolves — it names a person.
/watch — platformsSeven destinations in two aspect groups. Rendered from a single PLATFORMS table that lives
in one place in the site source (there is currently a second list in
apps/office/render_finessionals.py; see §8 contradiction C5).
16:9 — the programme as broadcast
| Platform | What it carries |
|---|---|
| YouTube | Anchor VODs, the long-form archive, weekly recaps |
| Twitch | Interactive build-along |
| Kick | The raw, unfiltered cut |
| X | Rapid takes, live debugging |
| Strategy and case-study segments |
9:16 — the vertical simulcast
| Platform | What it carries |
|---|---|
| TikTok | Vertical canvas simulcast + clips |
| Vertical canvas simulcast + clips |
Joint streams — the honest version. Every seat holder is required to have their own
Kick, Twitch, YouTube, Instagram and TikTok channels available for joint streams
(roles.json._shared_requirements.channels). That is five platforms, not seven. The page
must say *"episodes are also carried on participants' own Kick, Twitch, YouTube, Instagram
and TikTok channels"* and must not promise participant X or LinkedIn coverage, because
that was never a condition of any seat.
A participant channel list is only published with that participant's consent, and only
for people currently in the programme. When someone is eliminated their channel links come
down from the roster within one broadcast day. Elimination is public — that is disclosed to
them up front — but a permanent public directory of who was cut is a different thing from
the episode in which it happened.
Empty state: before channels exist, the platform tiles carry no links and the page says
so in one line: "Channels go live before Episode 1. Nothing is streaming yet." Do not
publish a dead link to a channel that has not been created.
Notify-me: an email field only. Single field, one purpose, covered by §3.7's privacy
terms at reduced scope: email + timestamp, used to send at most one message
("we are live"), deleted at the end of the season, with an unsubscribe link on every send.
/programme, /programme/roles, /programme/pay, /programme/rulesThese four pages restate nothing. They render from governance JSON, or they link.
/programme — the shapeThe competition structure, from pay.json.structure and season.py.ROUNDS:
gates passed, work published. Not opinion. (roles.json._shared_requirements.assessment.)
The run-of-show for an elimination Saturday is already written in
season.py.ELIMINATION_RUN_OF_SHOW (seven segments, 140 minutes). Render it from there.
/programme/roles — rendered from governance/roles.jsonOne card per entry. Use title, seat_title, one_line, owns, not_responsible_for /
not_delegable, requires. The Chairman entry is structurally different (type: chairman,
not a seat) and the page must render it differently rather than flattening it into a fifth
job — roles.json._why_different says exactly why, and collapsing that misrepresents the
programme.
Two things the renderer must handle:
disciplines and _hard_truth are public-safe and should be shown. They are the mosthonest copy in the file and they set expectations correctly.
links[] point at INTERNAL office routes — /pipelines, /calendar, /reports, /funnel, /gtm-strategy, /harness, /storyboard, /templates/preview,
/brand-book, /finessionals/producer-runbook, /finessionals/policy. Rendering these
verbatim on the public site emits dead or leaking links. **The public renderer must apply
an allow-list**, not a rewrite guess:
| roles.json href | Public site |
|---|---|
/finessionals | → /programme |
/finessionals/policy | → /programme/rules |
| everything else | dropped |
The allow-list is explicit and additive: an href not in it is not rendered. That way a new
internal link added to roles.json next month cannot leak by default.
accountability blocks: these carry the Upwork engagement terms and the
contract-end triggers. They are on the mandatory-disclosure list
(policy.json._NO_ONE_CAUGHT_OFF_GUARD), so they belong on /programme/rules, rendered
once, rather than duplicated into all four role cards where they are currently identical.
/programme/pay — rendered from governance/pay.jsonThe whole point of this page is that it is the only public copy of a contractual number.
Render: currency, engagement, the four tracks[] with weekly.R1 / R2 / final,
winner_earns, and the week_one_guarantee (status APPROVED, rule, why, funded_by).
Render the honest framing, not just the table. pay.json contains three notes that
exist precisely because a table alone misleads:
means fewer people at the same rate until the next step. This is on the disclosure list.
production system, 65 episodes of public portfolio, and the seat at the end.
programme_total ($20,000) is a budget figure, not a candidate-facing one. Show it only
if §6 decision D5 says to; a total spend number invites the wrong arithmetic on a page whose
job is to be honest about what one person earns.
The page is BLOCKED on pay.json.post_season_seat, which is NOT YET SET. See D1.
/programme/rules — rendered from governance/policy.jsonThe complete adjustment schedule, every line, with amounts — because
policy.json._NO_ONE_CAUGHT_OFF_GUARD.THE_ENFORCING_RULE states that a rule not disclosed
in writing before signing cannot be applied. Publishing the schedule is therefore not a
transparency gesture; it is how the schedule becomes enforceable at all.
Render from sections[]: each section's title, rationale, and each rule's rule,
deduction_usd or consequence, and cap where present. Plus:
elimination_and_proration — at-will, prorated, week-one grace period.question_policy — batched, not capped.velocity — the benchmark, stated as expectation.knowledge.required_understanding — the seven things everyone must be able to speak to.must_be_disclosed_before_signing list, as a checklist. This is the single mostuseful thing on the page for a candidate.
Draft status must be on the page. Both policy.json and pay.json carry
_status: DRAFT, counsel review pending. The page renders that status from the field —
it does not hardcode "draft" and it does not silently drop it when the field changes. Until
counsel signs, the page must say, in the same visual weight as the terms themselves, that
these are the terms as they stand and that the binding version is the signed contract.
**Do not render _three_flags_raised_2026_08_08, _upwork_reality, options, or
_counsel_must_still_review.** Those are the internal argument about the framework — the
analysis of its weaknesses, the classification exposure, the stacking arithmetic. They are
correct, they are valuable, and they are for counsel and the Chairman. A public page that
publishes its own legal exposure analysis is not being transparent; it is briefing the other
side of a dispute it has not had yet.
/shop — merch**Recommendation: do not build a cart. Use hosted commerce, and prefer a merchant of
record.**
The reasoning, stated plainly so it does not get relitigated:
| Owning it means owning | Cost of owning it |
|---|---|
| PCI-DSS scope | Card data touching our infrastructure pulls the whole stack into scope. SAQ A (redirect/hosted fields) is a form; SAQ D is an audit. There is no reason to move up that ladder to sell hoodies. |
| Tax | This show is international by design. That means VAT/GST registration thresholds in multiple jurisdictions, IOSS/OSS for the EU, UK VAT, and US destination-based sales tax across ~46 jurisdictions. Every one is a registration, a filing cadence and a liability. |
| Fulfilment | Stock, pick-pack, carriers, customs declarations, DDP vs DDU, returns across borders. |
| Chargebacks and fraud | A dispute process, evidence collection, and a loss rate. |
| Failure mode | A broken checkout during a live episode is a visible failure on the one surface where the audience is largest. |
None of that is the product. The product is the show.
Two tiers, pick one at §6 decision D4:
record and assumes the tax registration and remittance obligation entirely. Given that
the audience is deliberately international, this is the recommendation. Lower margin, near
zero operational and compliance surface. Fourthwall additionally does print-on-demand and
is built for creator/broadcast audiences, so it collapses commerce and fulfilment into one
vendor.
of record and therefore own the tax registrations above.
What the site owns: the /shop page — product photography, the story of each item, and
a link out. Checkout happens on the provider's domain. No cart state, no payment fields, no
card iframe on our pages. Stripe is already in the stack and this spec deliberately does
not use it directly for merch: having the API available is not the same as wanting the
merchant-of-record obligation.
Two hard constraints on what can be sold:
Participant releases are round-scoped and clearance does not carry forward between
rounds. A merch line featuring a competitor's face is a separate grant, obtained in
writing, per item, per round. This must be checked before a product is listed, not after
it sells.
a defect, and it is a defect that ships to a customer's wardrobe. Until a dark-ground
lockup exists, merch is light garments only, or it carries no mark.
Empty state: if there are no products, /shop does not exist in the nav. A shop page
saying "products coming soon" is the exact placeholder this spec forbids elsewhere.
/apply — intake, international by constructionPurpose statement, shown above the form, in the same visual weight as the fields:
We use what you send here to assess you for a seat on Finessionals Season 1 and to contact
you about it. Nothing else. We do not sell it, we do not share it with advertisers, and we
do not add you to a mailing list unless you separately ask us to.
| Field | Required | Notes |
|---|---|---|
| Name | yes | One field. Not first/last. Full UTF-8, no Latin-only validation, no length cap under 200 chars, no rejection of spaces, apostrophes, hyphens or single-word names. |
| Preferred name | no | What we should call you on air. |
| yes | The only required contact channel. | |
| Country | yes | ISO 3166-1 select. Rendered first, because it drives every label below it. |
| Town / city | yes | Free text. |
| State / province / region | conditional | Free text, never a dropdown. Label driven by country. Optional wherever the country has no such subdivision. |
| Postal code | optional, always | Free text. No regex. No length rule. No format hint that implies a shape. Label driven by country. |
| Time zone | yes | IANA select. Real operational need: the season runs across time zones and availability is a condition of every seat. |
| Track of interest | yes | The four from roles.json (type: seat), plus "not sure". |
| Channel handles | no | Kick, Twitch, YouTube, Instagram, TikTok. Owning these is a condition of the seat (_shared_requirements.channels), so the form should say that next to the fields rather than silently scoring on it. |
| Work links | yes | Portfolio, repo, reel. At least one. The qualification stage assesses past work. |
| Short answer | yes | One question, ~200 words. |
| Consent | yes | Separate, unticked by default, adjacent to the purpose statement. |
Deliberately absent: street address. We do not need it to assess an applicant, so we do
not collect it. If a merch order later needs a shipping address, the commerce provider
collects it, holds it, and we never touch it. The cheapest way to protect a piece of
personal data is to not have it.
Addresses are not a US shape with the rest of the world bolted on.
<select> of US states is wrong for the ~195 countries thatare not the US, and a country-conditional dropdown of subdivisions requires maintaining
a subdivision database that will be wrong for someone. Free text is always correct.
Department, Emirate, Oblast — and is hidden entirely where the country has no
meaningful subdivision for postal purposes (Singapore, Malta, Vatican City, and others).
territories have no postal code system at all (Ireland used none before Eircode; Hong Kong,
Macau, Panama, the UAE and others operate without one). Forcing a value invents data.
Validating a format rejects real people: UK postcodes are alphanumeric with a space,
Netherlands is 1234 AB, Canada is A1A 1A1, Japan is 123-4567, Brazil's CEP is
12345-678, and US ZIP+4 is nine digits with a hyphen. One regex cannot hold that, and
every regex that tries rejects somebody who lives somewhere.
(CA and most), PIN code (IN), CEP (BR), Eircode (IE).
of countries and silently mis-fills in the rest.
transliterate. A name typed in Cyrillic, Arabic or Han is a correct name.
The form is not "US with an international mode". Country comes first and everything below it
is a function of that answer.
This is a public form collecting personal data from people worldwide. Vagueness here is the
failure. Four things must be true, stated on the form and on /privacy:
whole purpose. Using the data for anything else — a marketing list, a future season, a
dataset — requires a separate, specific, unticked opt-in on the form, and applicants
who decline it are assessed identically.
(i.e. by 9 Jan 2028), unless they opted in to future consideration.
counsel sets for contract and payment records, which is longer and driven by tax and
accounting obligations, not by us.
policy with no cron behind it is a sentence, not a control.
applicant's own request prior to entering into a contract**. For anything beyond that
— keeping someone on file, marketing — the basis is consent, captured separately and
withdrawable. **Which regimes apply, and whether an EU/UK representative or a specific
notice form is required, is a counsel question and is not settled here.** This document
states the design; it does not assert a legal conclusion, and nothing in it is legal
advice. The design is chosen to be defensible under a strict reading precisely because we
do not yet know which readings apply — it costs little to collect less and delete on
schedule, and it costs a great deal to discover we should have.
privacy@<domain> — printed on the form itself and on /privacy, not buried in a policy. Deletion honoured within 30 days, and
confirmed by reply. Also available: a copy of what we hold, and correction. The path must
work for someone who applied once and never heard back — that is the person most likely
to want it, and the person least likely to have an account.
Storage: applications go to a single store with a named owner and access limited to the
people assessing them. Never into git. No PII in the repository, ever, including in
fixtures, test data, or a "sample submission" committed for development. The static site
holds no application data; the form POSTs to an endpoint (§7) that writes directly to the
store.
Anti-spam: rate limiting and a honeypot field. No third-party CAPTCHA — it adds a
third-party data flow to a form whose entire design premise is minimising data flows.
policy.json._what_this_blocks: *"No offer goes out until the post-season seat value is
set."* The post-season seat is the actual prize and it is NOT YET SET. Until D1 resolves,
/apply may collect expressions of interest — clearly labelled as such, with no terms
attached and no implication of an offer — or it stays closed. It must not present the full
terms as complete while a mandatory disclosure item is blank, because the disclosure rule is
self-enforcing: an incomplete disclosure weakens the terms it was meant to establish.
/privacy and /terms/privacy is the long form of §3.7: what is collected on each form (apply, notify-me),
purpose, retention rule with dates, lawful basis, the deletion/access/correction path, who
it is shared with (the commerce provider for orders; the hosting provider by necessity;
nobody else), and cookies.
Cookies: none beyond what is strictly necessary. No analytics that sets a cookie or
fingerprints, no ad pixels, no third-party embeds that phone home on load. Platform embeds
on /watch are click-to-load — a static thumbnail until the visitor asks for the player.
That removes the consent banner entirely, which is both better for the visitor and one fewer
thing to get wrong. If analytics is wanted, use a server-side, cookieless, aggregate
counter, and say so on this page.
/terms covers the site. **Participant terms are a different document, they are signed, and
they are not published here.**
| Site surface | Renders from | Rule |
|---|---|---|
/episodes, /episodes/<n> | engine/lib/season.py → episodes() | Generated. Zero hand-authored episodes. Adding one means editing season.py and nothing else. |
| Saturday streams | season.py → saturdays() | Interleaved, visually distinct, not counted as episodes |
| Week titles and themes | season.py → WEEKS, ROUNDS | |
| Elimination run-of-show | season.py → ELIMINATION_RUN_OF_SHOW | |
| What airs from each episode | season.py → DERIVATIVES | VOD / clips / podcast / recap, with their timings |
/programme/roles | governance/roles.json | Link allow-list applied (§3.5) |
/programme/pay | governance/pay.json | The single public copy of a pay figure |
/programme/rules | governance/policy.json | Public subset only (§3.5) |
| Track names | season.py.TRACKS ∩ roles.json[type=seat] | Must agree; a mismatch fails the build |
| Brand labels | engine/lib/episode_detail.py → BRANDS via season.py | |
| Colour and type | governance/brand.json (light inversion, §4.9) | |
| The lockup | obs-setup/brand/finessionals-color-on-light.png | Light grounds only |
Nothing renders from apps/office/. The office is a different audience with a different
tone and, as §8 shows, some of its copy already disagrees with the data. The public site
reads the same sources the office reads; it does not read the office.
The site build must fail — not warn — when:
season.episodes() does not yield exactly 65 dicts.brand, block, led_by or date.BLOCK_GLOSS.season.py.TRACKS and roles.json seat titles disagree.season.py.ROUNDS rates and pay.json.tracks[].weekly disagree — **see C8, this iscurrently two sources for one contractual number.**
href is outside the public route set (catches an internal office link leaking through roles.json).
governance/releases/ appears in the output.
/releases in any form.Gate 7 and 8 are the ones that matter most, and they are cheap. Every bug becomes a
permanent gate — that is already the house rule and it applies here.
Before 12 October 2026, nothing has aired and no clip exists. The site says that.
| Surface | Wrong | Right |
|---|---|---|
| Episode card | "0 views" · "Coming soon" | "Scheduled — Mon 12 Oct 2026" |
| Episodes index | "65 episodes available" | "65 episodes scheduled. None have aired yet." |
| Clips section | An empty grid, or stock imagery | The section does not render |
| Platform tiles | Dead links to channels that do not exist | Unlinked tiles + "Channels go live before Episode 1" |
| Follower / viewer counts | Any number | The component does not exist yet |
| Shop | "Products coming soon" | /shop is absent from the nav |
| Roster | Placeholder avatars, "TBA ×12" | The section does not render until people are signed and released |
The rule: a component that has no data does not render. It does not render a skeleton, a
placeholder, or a zero. A visitor should never be able to mistake an empty state for a
failed load, and should never see a count that is not real.
The corollary: an empty site before launch is correct, not embarrassing. A site showing
"0 episodes · 0 clips · 0 followers" is a site announcing it has nothing. A site showing
"Season 1 begins 12 October 2026 — here is the full 65-episode schedule" is a site with a
complete season on it before a frame has been shot. Same data, and the second one is true.
/releases — never, in any form. No link, no embed, no mirror, no summary that quotesone, no filename, no route that could be guessed. Participant agreements carry legal
names and signatures. They live in governance/releases/ and behind an office
authentication boundary. The build gate in §4.1 enforces this mechanically because a
convention is not a control.
to publish while they are in the programme.
(roles.json Producer). It is evidence, not content.
elimination rationale beyond what aired.**
policy.json — _three_flags_raised_2026_08_08, _upwork_reality, options, _counsel_must_still_review, and the per-rule _conflict
and _arithmetic_warning annotations. Correct, valuable, internal.
episode_detail's edit field — the production plan for which moments get clipped.em field — it names a person (§6 D6)./calendar, /harness, /reports, /funnel, /gtm-strategy, /pipelines, /storyboard, /templates/*, /brand-book,
/finessionals/producer-runbook, /finessionals/stream-guide,
/finessionals/participant-setup, /finessionals/broadcast-architecture. The
roles.json allow-list exists to stop these leaking automatically.
FINESSSIONALS.CO. They live in a rejected folder with a WHY.txt and must never reach a build.
logos of companies that have not agreed, no capability the pipelines registry marks as
missing. The grounded-truth rule that governs every other Temerarii surface governs
this one.
Colourway: light. Black script and red accent on white.
governance/brand.json (and its snapshot at obs-setup/brand/tokens.json) defines a dark
surface set — bg #0A0A0C, fg #FFFFFF. The site inverts it:
| Token | Site value | Source |
|---|---|---|
| ground | #FFFFFF | color.neutral.0 |
| ink | #000000 | color.ink.900 |
| body text | #1F2024 / #2A2C32 | color.ink.600/500 |
| accent | #EC1C24 | palette.accent |
| hairline | #D8DBE2 | color.neutral.100 |
| display / body | Tahoma | fonts.display/body |
| mono | JetBrains Mono | fonts.mono |
The red-contrast rule survives the inversion and must be restated for white.
brand.json says red is "accent / UI / large + headings only; NEVER body text" — that
note was written against #0A0A0C. On white it is still true and for the same reason:
#EC1C24 on #FFFFFF sits around 4:1, which passes AA for large text and UI components and
fails for small body text. Red is for headings, the live indicator, block badges,
borders and buttons. Body copy is ink. The secondary-colour cap (< 8% of any frame)
applies to layout too — red is punctuation, not a background.
No dark mode. This is a brand constraint, not a taste call. The only eligible lockup is
finessionals-color-on-light.png, and it is verifiably broken on dark grounds: the
transparency was made with a global white key that removed the fill inside the tail, so on
anything but a light ground the tail loses its structure and the black tail text sits on a
dark field. A missing logo is invisible; a black script on a black field is a defect people
screenshot. Until a dark-ground lockup exists, the site has one colourway and
prefers-color-scheme: dark is not honoured. Say that in a comment in the stylesheet so the
next person does not "fix" it.
No persistent header bug at small sizes. At bug scale the .CO tail becomes unreadable —
it needs roughly 30% of the width to hold a legible cap height, which is a banner, not a
bug. The site header carries the mark at a readable size or carries the wordmark as
text, set in Tahoma. It does not carry a shrunken lockup.
Aspect: the mark is 2.19:1, trimmed to content with no padding. Margins are decided in
CSS. Full-frame usage is centred at ~60% of the container width.
Accessibility: AA minimum throughout. Every interactive element keyboard-reachable.
Form labels bound to inputs — a country-driven label that changes must update the
<label for> text, and the change must be announced (aria-live), because a sighted user
sees "Postcode" replace "ZIP code" and a screen-reader user gets nothing unless it is
announced. Episode filters must be operable without a pointer.
Cloudflare, after broadcast testing. The Chairman's sequencing, and it is the right
order: a public site announcing air dates that the broadcast chain cannot yet hit is a
promise made before it can be kept. The broadcast is priority 0 in
pipelines.json (live-broadcast, three open blockers: multi-mic audio architecture, no
confidence feed, no guest control). Nothing on this site matters if the show cannot air.
Shape: static generation, same pattern as apps/office/build_office.py — a set of
renderers that read the sources in §4 and write HTML into a public directory, plus the build
gate in §4.1. Static output to Cloudflare Pages. One dynamic piece only: the form
endpoint, as a Pages Function / Worker that validates and writes to the application
store. No database on the read path — the entire read-side site is files.
Sequence:
| Phase | Gate to enter | Ships |
|---|---|---|
| 0. Blocked | — | Nothing. Broadcast testing is not complete; D1 (post-season seat) is unset. |
| 1. Skeleton | Broadcast pilot rehearsal passes (pipelines.json.live-broadcast.assessed_by) | /, /episodes, /episodes/<n>, /watch. Generated from season.py. No apply, no shop. Empty states everywhere they belong. |
| 2. Programme | D1 resolved; counsel has reviewed policy.json + pay.json | /programme, /programme/roles, /programme/pay, /programme/rules. |
| 3. Intake | Store provisioned with a named owner; deletion job scheduled and tested; /privacy live | /apply, /privacy, /terms. |
| 4. Commerce | D4 decided; provider onboarded; release clearance confirmed for any likeness | /shop. |
| 5. Live | Channels created; real watch URLs exist | Watch links populate. Episode cards gain VOD links as episodes actually air — the generator reads a links file, it does not pre-fill. |
Phase 3 does not ship before the deletion job is tested. A retention rule that has never
been observed to delete anything is not a control, and the first time it runs must not be
the first time anyone finds out it does not.
| # | Decision | Blocks | Notes |
|---|---|---|---|
| D1 | What is the post-season seat worth? Rate or salary, engagement type after the season, and whether it is guaranteed to the winner or offered at discretion. | /apply entirely; /programme/pay is incomplete without it | pay.json.post_season_seat = REQUIRED FROM THE CHAIRMAN — NOT YET SET. policy.json puts it on the mandatory pre-signing disclosure list, so an unset value is an incomplete disclosure. This is the single largest blocker on the site and it is not a site decision — it is commercial. |
| D2 | The domain. The lockup's tail reads .CO, implying finessionals.co. Is it registered? Is it the canonical domain, or does the site live under an existing property? | Everything — links, email addresses, the privacy contact, the Cloudflare setup | Also determines the privacy@ address in §3.7. |
| D3 | Commission the missing lockups? Dark-ground, script-only (no tail), all-white, and a vector. | Dark mode, any persistent bug, dark merch, the favicon | obs-setup/brand/README.md lists all four as still owed. Vector solves the transparency problem permanently. Until then the site is light-only, which is workable but is a constraint the Chairman should accept knowingly rather than inherit. |
| D4 | Commerce provider: merchant of record (Fourthwall / Lemon Squeezy / Paddle) or hosted storefront (Shopify)? | /shop | Recommendation: merchant of record, because the audience is international by design and MoR removes the multi-jurisdiction tax registration obligation entirely. |
| D5 | Publish programme_total ($20,000) publicly? | /programme/pay layout | Recommendation: no. It is a budget number on a page whose job is to be honest about what one person earns, and it invites arithmetic that misleads in both directions. |
| D6 | Are Executive Managers named publicly per episode? episode_detail carries an em field per episode. | /episodes/<n> | Recommendation: not until the person exists, is contracted, and has consented. Default to omitting the field. |
| D7 | Is the participant channel roster published, and does it come down on elimination? | /watch | Recommendation: publish only for people currently in the programme, remove within one broadcast day of elimination. Elimination is public in the episode; a permanent public list of who was cut is a different artefact. |
| D8 | Which counsel-reviewed privacy notice applies, and is an EU/UK representative needed? | /privacy sign-off | This spec designs to a strict reading deliberately. It does not, and cannot, determine which regimes apply. Counsel decides; this goes into the same review already blocking the participant release. |
| D9 | Does /apply open as "expressions of interest" before D1 resolves, or stay closed? | Phase 3 timing | Recommendation: closed. A form that collects hopeful people against terms that are not final is the thing _NO_ONE_CAUGHT_OFF_GUARD exists to prevent. |
| D10 | Reconcile "Director" vs "Producer". See C1 below. One name, everywhere. | /programme/roles, and the office | This is a naming decision only the Chairman can settle, and it is currently wrong in one of two places that both face candidates. |
| D11 | Episode length: 120 minutes or three hours? See C4. | /episodes/<n>, /watch | The public site should state a duration. It cannot state two. |
Found while specifying. Each one would produce a public page that disagrees with another
public page, so each needs settling before the corresponding surface ships.
C1 — "Director" does not exist in the roles registry.
apps/office/render_finessionals.py builds its entire ownership spine around a Director
("owns everything a production team owns", "produces, does not approve", OWNERSHIP,
LIFECYCLE, DAY_ONE). governance/roles.json has no such role: the seat is Producer,
seat_title Executive Producer, and pipelines.json.live-broadcast.owner_role is
"Producer". season.py assigns CHECKPOINT to "Producer". The registry and the schedule
agree with each other; the office page does not agree with either. The public site will use
roles.json, which means the office page and the public site would show different job
titles for the same seat, to the same candidate. → D10.
C2 — two incompatible week structures.
render_finessionals.py.WEEK describes Monday Kickoff / Tuesday Tech sprint / Wednesday
Creative sprint / Thursday Paired sprint / Friday Judgment & swap.
season.py.SPINE_A/SPINE_B describes Mon BRIEF · Tue BUILD · Wed CROSS · Thu GATE · Fri
CHECKPOINT, alternating with Mon ADJUST · Tue BUILD · Wed CROSS · Thu PUBLISH · Fri DEMO.
These are not two descriptions of one week; they are two different weeks. season.py is the
generator that 65 episodes come from, so it wins by construction — but the office page is
currently telling prospective hires something else.
C3 — episode count: 65 vs "roughly 85".
season.episodes() yields exactly 65 (verified). The office page prints 65 in its facts
strip and then says *"add pre-season casting and a pre-recorded contingency bank and the
season is roughly 85 episodes / 170 hours"*. **The public number is 65 aired weekday
episodes plus three Saturday streams.** 85 is a production planning figure that includes
material which may never air, and publishing it creates an expectation the schedule cannot
meet.
C4 — episode length: 120 minutes vs three hours.
render_finessionals.py.SEASON["minutes"] = 120, and its RUN_OF_SHOW totals exactly
02:00. But roles.json Producer requires *"a connection that survives a three-hour
show"*. Either the run-of-show is 120 minutes and the requirement is (sensibly) padded for
pre-flight and post-show, or the show is longer than the run-of-show says. The public site
must state one duration. → D11.
C5 — platform count contradicts itself inside one file.
render_finessionals.py renders len(PLATFORMS) = 6 in the facts strip (with TikTok and
Instagram collapsed into one entry), while its DAY_ONE copy says *"everything airs live to
five platforms"*. This spec counts seven destinations (YouTube, Twitch, Kick, X,
LinkedIn, TikTok, Instagram) because TikTok and Instagram are separate places a viewer
goes even if they share a vertical canvas. Whatever the number is, it must come from one
list — which is why §3.4 requires a single PLATFORMS table.
C6 — participant channels cover five platforms, not seven.
roles.json._shared_requirements.channels requires Kick, Twitch, YouTube, Instagram and
TikTok — no X, no LinkedIn. A /watch page promising joint streams across all seven would
promise something that was never a condition of any seat. §3.4 states the five explicitly.
C7 — the season does not end on 8 January.
The last weekday episode is Fri 8 Jan 2027, but season.saturdays() puts the **Finale on
Sat 9 Jan 2027**. A public "12 Oct 2026 – 8 Jan 2027" window omits the finale, which is the
episode most people will want the date of. The public season window is **12 Oct 2026 –
9 Jan 2027**.
C8 — the pay ladder exists in two files.
governance/pay.json.tracks[].weekly and season.py.ROUNDS[].rates both carry the rates.
They agree today — verified: 150/125/125/100, then 240/200/200/160, then 456/380/380/304 —
but that is two independent sources for a contractual number, which is precisely the
failure roles.json._pay_note was written to prevent. One should derive from the other, and
until it does, the build gate in §4.1 must fail on any disagreement. The site renders from
pay.json only.
C9 — the terms the site would publish are DRAFT.
Both policy.json._status and pay.json._status read DRAFT, counsel review pending —
while policy.json._SELECTED records the framework as CANONICAL by Chairman decision of
2026-08-08. Both are true (decided internally, not yet reviewed), but a public page cannot
present a counsel-pending framework as settled terms. §3.5 renders the status from the field
rather than hardcoding it, so the page corrects itself the moment counsel signs.
C10 — week_one_guarantee is APPROVED inside a DRAFT file.
pay.json.week_one_guarantee.status = "APPROVED by the Chairman 2026-08-08", sitting in a
file whose _status is DRAFT. It is the most candidate-favourable term in the structure and
therefore the one most worth publishing early. Confirm it is safe to state as approved while
the surrounding document is still under review, or hold the whole page to phase 2.
Thirteen routes. Sixty-five episode pages that nobody writes. One copy of every pay figure,
and it is not on this site — it is in governance/pay.json, and the site is a window onto
it. A form that assumes nothing about where you live. A shop that does not process a card.
No participant's signature anywhere near it. And, before October, an honest and complete
season schedule with nothing pretending to have happened yet.