Back Office · office.temerarii.xyz
FINESSIONALS-SITE.md

← all docs

FINESSIONALS-SITE.md — specification for the public Finessionals website

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.


1. Context — why this exists

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:

SurfaceWhat it isAudience
apps/office/public/finessionals/the office hub — ownership split, runbooks, setup sheetsoperators, prospective hires
governance/*.jsonroles, pay, policy, pipelines — the show as datathe build
engine/lib/season.pythe schedule — 65 episodes, generatedevery 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:

  1. What is this and when can I watch it? → episodes, platforms, times.
  2. Where do I watch it? → seven destinations plus every participant's own channels.
  3. Can I buy something? → merch.
  4. Can I be involved? → intake, international by construction.

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 governing constraint on this whole document

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.


2. Information architecture

/                       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.


3. Page-by-page spec

3.1 / — the show

Purpose: a cold visitor understands what Finessionals is inside fifteen seconds, and

knows the date.

Blocks, in order:

  1. Mark + one sentence. The lockup, centred, on white. One line: what the show is.

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."*

  1. When. Season opens Sunday 11 Oct 2026; Episode 1 airs Monday 12 Oct 2026. Before the

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."

  1. Where to watch. Seven logos, linking to /watch. Not a wall of embeds.
  2. The four tracks. Name, one line each, from roles.json[].one_line. Links to

/programme/roles.

  1. This week. The five episodes of the current week, from season.py. Pre-season this

block shows week 1 with a "starts in N days" label — it is a schedule, not a claim that

anything has aired.

  1. Apply / Shop. Two calls to action. The apply CTA is gated on §6 decision D1.

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.


3.2 /episodes — all 65, browsable

This 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:

BlockCountWho leadsOne-line public gloss
BRIEF7Executive ManagerThe week's scope, agreed on camera and written down
BUILD13Technical OperatorTerminal only. The unglamorous middle, shown rather than skipped
CROSS13counterpartsPeople doing each other's jobs, badly, in good faith
GATE7Technical OperatorThe gates run live. Failures on screen, in red
CHECKPOINT7ProducerWhat is actually done versus what was claimed on Monday
ADJUST6Executive ManagerWhat the checkpoint changed, and what got cut
PUBLISH6Technical OperatorIt ships or it does not. Every item traces to a registry entry
DEMO6each participantEveryone 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.


3.3 /episodes/<n> — one episode

65 static pages, generated from the same iterator. Contents:

  • Episode number, full date, weekday, week number and week title.
  • Round, round theme, and how many seats are still in play that week.
  • Block badge and who leads it (led_by).
  • Brand for the episode (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.

  • What happens — the live string from the spine/detail. This is the public-facing

description and it is already written. Do not rewrite it on the site.

  • Previous / next episode.
  • Watch links, only if they exist. Otherwise a scheduled-date line.

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.


3.4 /watch — platforms

Seven 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

PlatformWhat it carries
YouTubeAnchor VODs, the long-form archive, weekly recaps
TwitchInteractive build-along
KickThe raw, unfiltered cut
XRapid takes, live debugging
LinkedInStrategy and case-study segments

9:16 — the vertical simulcast

PlatformWhat it carries
TikTokVertical canvas simulcast + clips
InstagramVertical 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.


3.5 /programme, /programme/roles, /programme/pay, /programme/rules

These four pages restate nothing. They render from governance JSON, or they link.

/programme — the shape

The competition structure, from pay.json.structure and season.py.ROUNDS:

  • 13 weeks, 65 weekday episodes, three Saturday streams.
  • Round 1 — weeks 1–4, "Learn the machine", 3 per track, 12 on air.
  • Round 2 — weeks 5–8, "Run it for real", 2 per track, 8 on air.
  • Final — weeks 9–13, "Own it", 1 per track, 4 on air.
  • Four tracks: Technical Operator, Creative Lead, Producer, Executive Manager.
  • Elimination is decided against a record read from the system — registry entries executed,

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.json

One 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:

  1. disciplines and _hard_truth are public-safe and should be shown. They are the most

honest copy in the file and they set expectations correctly.

  1. 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 hrefPublic site
/finessionals→ /programme
/finessionals/policy→ /programme/rules
everything elsedropped

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.json

The 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:

  • The rate follows the calendar, not the number of seats remaining. An early elimination

means fewer people at the same rate until the next step. This is on the disclosure list.

  • The stipend is not the compensation. The full consideration is paid training on a real

production system, 65 episodes of public portfolio, and the seat at the end.

  • Week one is paid in full regardless of elimination, and carries no adjustments.

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.json

The 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.
  • The full must_be_disclosed_before_signing list, as a checklist. This is the single most

useful 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.


3.6 /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 owningCost of owning it
PCI-DSS scopeCard 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.
TaxThis 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.
FulfilmentStock, pick-pack, carriers, customs declarations, DDP vs DDU, returns across borders.
Chargebacks and fraudA dispute process, evidence collection, and a loss rate.
Failure modeA 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:

  • Merchant of record (Fourthwall, Lemon Squeezy, Paddle) — the provider is the seller of

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.

  • Hosted storefront (Shopify) — more control, better on brand, but we remain the seller

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:

  1. No participant likeness on any product without release clearance for that use.

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.

  1. The lockup is light-grounds-only. See §4.9. A black tee carrying the current mark is

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.


3.7 /apply — intake, international by construction

Purpose 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.

Fields

FieldRequiredNotes
NameyesOne 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 namenoWhat we should call you on air.
EmailyesThe only required contact channel.
CountryyesISO 3166-1 select. Rendered first, because it drives every label below it.
Town / cityyesFree text.
State / province / regionconditionalFree text, never a dropdown. Label driven by country. Optional wherever the country has no such subdivision.
Postal codeoptional, alwaysFree text. No regex. No length rule. No format hint that implies a shape. Label driven by country.
Time zoneyesIANA select. Real operational need: the season runs across time zones and availability is a condition of every seat.
Track of interestyesThe four from roles.json (type: seat), plus "not sure".
Channel handlesnoKick, 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 linksyesPortfolio, repo, reel. At least one. The qualification stage assesses past work.
Short answeryesOne question, ~200 words.
ConsentyesSeparate, 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.

Why the address block is shaped this way

Addresses are not a US shape with the rest of the world bolted on.

  • Region is free text. A <select> of US states is wrong for the ~195 countries that

are 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.

  • The region label changes with country — State, Province, Region, County, Prefecture,

Department, Emirate, Oblast — and is hidden entirely where the country has no

meaningful subdivision for postal purposes (Singapore, Malta, Vatican City, and others).

  • Postal code is optional everywhere and validated nowhere. Several countries and

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.

  • The postal-code label changes too — ZIP code (US), Postcode (UK/AU/NZ), Postal code

(CA and most), PIN code (IN), CEP (BR), Eircode (IE).

  • No autofill of region from postal code. That mapping only exists cleanly in a handful

of countries and silently mis-fills in the rest.

  • Accept non-Latin script everywhere. Store as UTF-8, render as submitted, and never

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.

Privacy — concretely

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:

  1. Stated purpose. Assessment for a Season 1 seat, and contact about it. That is the

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.

  1. Retention period, stated as a date rule, not "as long as necessary".
  • Not shortlisted: deleted 90 days after the application is closed.
  • Shortlisted but not seated: deleted 12 months after the season ends

(i.e. by 9 Jan 2028), unless they opted in to future consideration.

  • Seated: application data merges into the contract record and follows the retention that

counsel sets for contract and payment records, which is longer and driven by tax and

accounting obligations, not by us.

  • The rule is enforced by a scheduled deletion job, not by intention. A retention

policy with no cron behind it is a sentence, not a control.

  1. Lawful basis. For assessing an applicant, the basis is the **steps taken at the

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.

  1. Deletion path, one hop. A named address — 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.

The apply page is gated

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.


3.8 /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.**


4. Data sources — what renders from what

Site surfaceRenders fromRule
/episodes, /episodes/<n>engine/lib/season.py → episodes()Generated. Zero hand-authored episodes. Adding one means editing season.py and nothing else.
Saturday streamsseason.py → saturdays()Interleaved, visually distinct, not counted as episodes
Week titles and themesseason.py → WEEKS, ROUNDS
Elimination run-of-showseason.py → ELIMINATION_RUN_OF_SHOW
What airs from each episodeseason.py → DERIVATIVESVOD / clips / podcast / recap, with their timings
/programme/rolesgovernance/roles.jsonLink allow-list applied (§3.5)
/programme/paygovernance/pay.jsonThe single public copy of a pay figure
/programme/rulesgovernance/policy.jsonPublic subset only (§3.5)
Track namesseason.py.TRACKS ∩ roles.json[type=seat]Must agree; a mismatch fails the build
Brand labelsengine/lib/episode_detail.py → BRANDS via season.py
Colour and typegovernance/brand.json (light inversion, §4.9)
The lockupobs-setup/brand/finessionals-color-on-light.pngLight 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.

4.1 The build gate

The site build must fail — not warn — when:

  1. season.episodes() does not yield exactly 65 dicts.
  2. Any episode lacks brand, block, led_by or date.
  3. A block appears that has no entry in BLOCK_GLOSS.
  4. season.py.TRACKS and roles.json seat titles disagree.
  5. season.py.ROUNDS rates and pay.json.tracks[].weekly disagree — **see C8, this is

currently two sources for one contractual number.**

  1. Any rendered href is outside the public route set (catches an internal office link

leaking through roles.json).

  1. Any string matching a participant name, an email address, or a file path under

governance/releases/ appears in the output.

  1. Any page references /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.


5. Honest empty states

Before 12 October 2026, nothing has aired and no clip exists. The site says that.

SurfaceWrongRight
Episode card"0 views" · "Coming soon""Scheduled — Mon 12 Oct 2026"
Episodes index"65 episodes available""65 episodes scheduled. None have aired yet."
Clips sectionAn empty grid, or stock imageryThe section does not render
Platform tilesDead links to channels that do not existUnlinked tiles + "Channels go live before Episode 1"
Follower / viewer countsAny numberThe component does not exist yet
Shop"Products coming soon"/shop is absent from the nav
RosterPlaceholder 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.


6. What must NOT be on this site

  1. /releases — never, in any form. No link, no embed, no mirror, no summary that quotes

one, 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.

  1. Any participant's personal data beyond a name and channel handles they have consented

to publish while they are in the programme.

  1. The incident log. It is what payment disputes are settled from

(roles.json Producer). It is evidence, not content.

  1. **Anything about a pay dispute, an adjustment applied to a named person, or an

elimination rationale beyond what aired.**

  1. The internal analysis in 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.

  1. episode_detail's edit field — the production plan for which moments get clipped.
  2. The em field — it names a person (§6 D6).
  3. Internal office routes. /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.

  1. Credentials, stream keys, relay endpoints, scene collections, the OBS setup.
  2. The rejected 3-S artwork. Three supplied lockups render FINESSSIONALS.CO. They live

in a rejected folder with a WHY.txt and must never reach a build.

  1. The mark on any dark ground. See §4.9 / §7 D3.
  2. Any claim the system cannot back. No invented viewer numbers, no "trusted by", no

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.

  1. A cart, a card field, or a payment form. §3.6.
  2. PII in the repository. Including fixtures and sample submissions.

7. Brand and presentation

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:

TokenSite valueSource
ground#FFFFFFcolor.neutral.0
ink#000000color.ink.900
body text#1F2024 / #2A2C32color.ink.600/500
accent#EC1C24palette.accent
hairline#D8DBE2color.neutral.100
display / bodyTahomafonts.display/body
monoJetBrains Monofonts.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.


8. Deployment and build sequence

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:

PhaseGate to enterShips
0. Blocked—Nothing. Broadcast testing is not complete; D1 (post-season seat) is unset.
1. SkeletonBroadcast 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. ProgrammeD1 resolved; counsel has reviewed policy.json + pay.json/programme, /programme/roles, /programme/pay, /programme/rules.
3. IntakeStore provisioned with a named owner; deletion job scheduled and tested; /privacy live/apply, /privacy, /terms.
4. CommerceD4 decided; provider onboarded; release clearance confirmed for any likeness/shop.
5. LiveChannels created; real watch URLs existWatch 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.


9. Open decisions requiring the Chairman

#DecisionBlocksNotes
D1What 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 itpay.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.
D2The 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 setupAlso determines the privacy@ address in §3.7.
D3Commission the missing lockups? Dark-ground, script-only (no tail), all-white, and a vector.Dark mode, any persistent bug, dark merch, the faviconobs-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.
D4Commerce provider: merchant of record (Fourthwall / Lemon Squeezy / Paddle) or hosted storefront (Shopify)?/shopRecommendation: merchant of record, because the audience is international by design and MoR removes the multi-jurisdiction tax registration obligation entirely.
D5Publish programme_total ($20,000) publicly?/programme/pay layoutRecommendation: 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.
D6Are 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.
D7Is the participant channel roster published, and does it come down on elimination?/watchRecommendation: 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.
D8Which counsel-reviewed privacy notice applies, and is an EU/UK representative needed?/privacy sign-offThis 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.
D9Does /apply open as "expressions of interest" before D1 resolves, or stay closed?Phase 3 timingRecommendation: closed. A form that collects hopeful people against terms that are not final is the thing _NO_ONE_CAUGHT_OFF_GUARD exists to prevent.
D10Reconcile "Director" vs "Producer". See C1 below. One name, everywhere./programme/roles, and the officeThis is a naming decision only the Chairman can settle, and it is currently wrong in one of two places that both face candidates.
D11Episode length: 120 minutes or three hours? See C4./episodes/<n>, /watchThe public site should state a duration. It cannot state two.

10. Contradictions found in the existing data

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.


11. Summary of what this site is

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.