Site design reference

September 11, 2026 update: the approved hero mockups supersede the full-size shell masthead and image-free lead descriptions below. Header now uses a compact identity beside navigation; Home and Schedule own EventHero with style-specific artwork and component tokens. The hero alone permits a surface-color opacity veil for text readability. See the dated update in the binding design brief.

Everything the look of a client site is made of: The six site styles, the type, the illustrations, the headers, the page templates, the colour, the tokens under all of it, what prints, and where a developer extends it.

This is the deep end, and you do not need it to run a site. ADMIN_GUIDE.md is the operator's path — pick a style, add your logo, set your colour, preview, publish — and it is a complete answer for almost every event. Come here when you want to know what a control is doing underneath, when a recommended configuration is not quite right, or when you are adding to the system rather than using it.

Two audiences, one document. Where a section says Staff it is about a control somebody clicks; where it says Engine it is about the mechanism, and the names in it are internal ones (interface-guidelines.md, Vocabulary).

How a look is decided#

One chain, four links, and each one only overrides what it names:

  1. The design tokens (design/tokens/*.json) define every scale, contract, and primitive the system has. Nothing outside them is a value.
  2. The site style picks a base: Two authored palettes, a type pairing, a shape, a header, a set of illustrations, and its own remaps of the tokens above.
  3. The options inside that style retune it — the heading face, the header style, the schedule style. An option remaps tokens the style already declares. It never invents a name.
  4. The overrides are the exceptional path: One colour, one font role, one radius, named outright.

Whatever comes out of that is resolved once, in one place, and everything reads the result: The public site, the admin's page preview, the emails, and the PDF handout.

Engine. The stored document is config/theme. packages/shared/src/theme.cjs is the single resolver — the browser, the build-time generator, and the publish gate all call it, so none of them can disagree about what a document means. scripts/lib/tokens.cjs turns the resolved answer into apps/web/src/generated/theme.css at build time; apps/web/src/lib/themeRuntime.js turns the same answer into a <style> element at run time, so a publish restyles an open page with no deploy. data-theme, data-mode, and data-motif-set on the root element pick which blocks win.

Nothing here is per-client code. A client's whole visual identity is one Firestore document. There is no branch, no fork, and no build flag per event.

The six site styles#

Every style is finished work: Two authored palettes, a type pairing that has been set against real event content, its own devices, and its own answers in every option group. None of them is a skin over another, and none of them is a lesser version of the default. What they differ in is what an event is for.

The catalog is one flat, ordered list: Six styles, every one of them finished work, with the picker's order as the only ranking between them.

Institutional — the default#

The public record of a public event. Plain, patient, unambiguous, with nothing between the reader and the information.

  • Suits: A government body, a university, a professional association, a public meeting, anything where the reader is checking a fact rather than being persuaded.
  • Type: Merriweather over Public Sans. A serif that stays legible at small sizes over a face designed for public documents.
  • Shape: Near-square corners, flat surfaces, comfortable spacing.
  • Devices: The institutional letterhead header; the posted agenda for the schedule.
  • Illustrations: None.
  • Why it is the default: It is the plainest of the six, it targets the highest accessibility bar, and it is the look a client is least likely to have to undo. A fresh deployment starts here.
  • Deeper controls: The heading face can move to Source Serif 4 (warmer) or to Public Sans bold (drops the serif entirely, for an event that is not a document). The header can become a two-part lockup, for an event run by two organizations, or a compact standing head. The schedule can number its agenda items, which a formal proceeding often wants. A long read opens plain; a quote is a ruled minute or plain rules; the directory is ruled entries or portrait plates; a table takes the full grid; a section boundary keeps its folio, drops it, or moves it to the margin.

Newsroom#

A well-made news site on a good day. Named sections, one strong rule each, numbers that never borrow the headline face.

  • Suits: A conference with a programme worth reading, a summit, a festival — anything with editorial ambition and a lot of sessions.
  • Type: Fraunces over Newsreader, with IBM Plex Sans carrying data and IBM Plex Mono carrying values. Four roles, four jobs.
  • Shape: The small radius, flat surfaces, comfortable spacing.
  • Devices: The rule-bounded bar header; the hairline row for a session.
  • Illustrations: None.
  • Deeper controls: The heading face can move to Newsreader at display weights (one family throughout, quieter) or to Archivo bold condensed (more sessions per screen in a headline). The header can stack with a deck, which suits an event whose name needs a sentence after it, or become section-aware so the running header names where the reader is. Sessions can run as "lead and rest", which gives the first session of a day the room a keynote deserves. A long read opens on a standfirst; a quote is ruled with an opening mark, a side rule, or the mark alone; the directory is a portrait shelf or tall portraits; a table closes its head with a strong rule; a section boundary keeps its folio, drops it, or moves it to the margin.

Zine#

Made by hand, at a copier, the week before. One loud display face, an even rhythm under it, and an accent spent twice a page.

  • Suits: An unconference, a community gathering, a student event, a workshop series — anything whose character is that it is not corporate.
  • Type: Karrik for headings over Source Sans 3 for reading, with Fragment Mono carrying times, room codes, and identifiers. The mono is the value face, not the page face: A long session description set in a typewriter face is slow to read at any length.
  • Shape: Sharp corners, loose spacing.
  • Devices: The full-sheet header; the flat block for a session.
  • Illustrations: None.
  • The one exception in the whole system: Zine may print a session block off-register, like a stamp that missed. It is the only place the design brief grants a deliberate imperfection, and it is off by default — a printing artefact is something a client turns on, not something they discover on their own schedule page.
  • Deeper controls: The heading face can move to Bagnard or Avara. The header can become a stacked block or a boxed bill. The session block can take the off-register stamp or the struck folio. Pen marks — the hand that went over the page afterwards — can be turned on, and they are their own control, so a client can take the stamp without the pen. A long read opens plain; a quote is handwritten, a toner block in the poster face, or a cut-out beside a strong rule; the directory is a contact sheet or cut-outs; a table closes its head with a strong rule; a section boundary keeps its folio or drops it.

Broadsheet#

The paper of record. The masthead is the loudest thing on the page, and rules do the dividing.

  • Suits: An annual convening with weight behind it, an anniversary edition, an event that wants to look like it has happened many times before.
  • Type: Libre Caslon Display over Libre Caslon Text, with a serif carrying data too. One voice, all the way down.
  • Shape: Sharp corners, flat surfaces, tight spacing — a broadsheet fits a lot on a page.
  • Devices: The full-measure header; the ruled programme for the schedule.
  • Illustrations: None.
  • Deeper controls: The heading face can move to Libre Baskerville or Spectral semibold. The header can centre itself under a double rule, or compact to the left with a standing edition line. The schedule can drop to an agate block — the dense small-type listing a printed programme uses when the day is long. A long read opens on a drop cap; a quote is centred with an opening mark or set beside a column rule; the directory is an agate directory or plate portraits; a table takes the full hairline grid; a section boundary keeps its folio, drops it, or moves it to the margin.

Field Guide#

A naturalist's expedition handbook. Plates, specimen labels, and observation notes, with the linework carrying the character.

  • Suits: A field school, an environmental convening, a residency, a science festival — an event about looking closely at things.
  • Type: Besley over Vollkorn, with IBM Plex Mono for data.
  • Shape: Sharp corners, comfortable spacing, and the paper tone available as an advanced setting.
  • Devices: A plain ruled title head; the specimen label for a session.
  • Illustrations: The botanical set exists for this style and ships off. Drawings on every page of a real programme read as decoration rather than as observation. Turn them on for an event where they mean something.
  • Deeper controls: The heading face can move to Vollkorn at display size or to Spectral semibold. The header can become a framed title page or a plate-and-label lockup. Sessions can run as a field-notes column. A pencil line can be turned on in the margins. The empty-state plate stays framed whatever you pick, because a blank sheet really is one. A long read opens on a drop cap; a quote is a field note or a pressed page; the directory is specimen plates or a field list; a table closes its head with a hairline; a section boundary keeps its folio, drops it, or moves it to the margin.

Atlas#

A mapped city on its transit network. A survey sheet and a departure board at once: Lines, stations, transfers.

  • Suits: A multi-venue event, a city-wide festival, anything where getting from one thing to the next is part of the problem.
  • Type: Overpass over Libre Franklin, with Overpass Mono carrying times and codes.
  • Shape: Sharp corners, flat surfaces, tight spacing.
  • Devices: The corner title block; the departure board for the schedule.
  • Illustrations: The cartographic set, on by default for this style.
  • Where the grid is drawn: The coordinate grid and the route marks sit on the schedule surface only. A grid behind the about page, the speaker bios, and the code of conduct is texture; a grid behind a timetable is a reading device.
  • Deeper controls: The heading face can move to Libre Franklin extrabold or Archivo Condensed. The header can take a scale line or become a departure-board header. The schedule can run as a line diagram or a gazetteer list. A long read opens plain; a quote is a route line or a boxed sign; the directory is a gazetteer or a station index; a table takes the departure board's full grid; a section boundary keeps its folio or drops it.

Header style#

Four header styles, and a site picks one. The header is the site's identity, and it repeats on every page — which is exactly why it is not a heading: Every page owns its own h1, and the header sits outside it. Each site style also names its own three, tuned to it — Broadsheet's full measure, Newsroom's rule-bounded bar, Zine's full sheet — and those are the ones the picker offers once a style is chosen.

StyleWhat it drawsReach for it when
StandardThe event name at normal weight, the dates and place under it, the navigation.The default. The identity is present and the page's own headline leads.
MastheadThe rule-bounded title block at display size: Name, dates, edition line.The event's name is the point — a front page, a single-day event, an anniversary.
CompactThe event bar: Name and navigation on one line.A long document site where every pixel above the text is spent.
MinimalThe mark and the navigation. No name.The event's mark is well known, or the page is a utility.

A page may override the site's choice, and one usually should not. The site-wide value is what makes the header furniture a reader stops noticing; a page that differs is asking to be noticed, which is right for a front page and wrong for a policy page.

Engine. config/theme.header names one of standard, masthead, compact, minimal; a cmsPages document may state its own layout.header. resolveHeader(themeHeader, pageHeader) returns the one to draw — page first, then theme, then the base. A stored value the validator should never have accepted renders the base rather than no header at all. A style's own three are the nameplate option group (optionPicks.nameplate), which is why the internal name is nameplate and the label is Header style. Nameplate is unchanged in shape; it is now the device the masthead treatment draws rather than the shell's only header.

Schedule style#

How a session is set out on the schedule. Every site style names its own choices, because a session listing is one of the two places a style is most itself — the header is the other, which is why every style above names exactly one of each under Devices — the posted or numbered agenda for Institutional, the hairline row or the lead-and-rest ranking for Newsroom, the flat block, the stamped block, or the struck folio for Zine, the ruled programme or the agate block for Broadsheet, the specimen label or the field-notes column for Field Guide, the departure board, line diagram, or gazetteer list for Atlas.

Whichever is picked, the day still reads twice and both readings are first-class: A grid at wide viewports when the event lists tracks, and a time-ordered list everywhere else. A schedule style retunes how a session is drawn. It never decides which of those two views a reader gets.

Engine. The component option group (optionPicks.component). Like every option, it remaps tokens the style already declares — the session contracts in tier 3 — and introduces no property name of its own.

Typography#

Four roles, never family names. A component asks for a role and gets whatever the resolved style says that role is.

RoleWhat it carries
headingEvery heading, and the masthead.
bodyEverything a person reads as language.
dataThe voice beside the text: Labels, folios, captions.
monoAnything a person would copy, paste, or compare character by character.

There is no fifth role. Zine's handwritten callout runs on a component token, which is a contract for one device rather than a role.

The scale is fluid and has eight steps — nameplate, h1, h2, h3, lead, body, caption, folio — and each step carries its own size, line height, and tracking together. Never pick a size without its leading.

The bundled sets are the only faces a site can use, and they are self-hosted. No page on a client site asks an external font service for anything, ever: Source Serif 4, Source Sans 3, Caveat, Libre Caslon Display, Libre Caslon Text, Libre Baskerville, Spectral, Fraunces, Newsreader, IBM Plex Sans, IBM Plex Mono, Archivo Condensed, Merriweather, Public Sans, Karrik, Bagnard, Avara, Fragment Mono, Besley, Vollkorn, Overpass, Overpass Mono, Libre Franklin, Cabin.

Heading-face options are the sanctioned way to change type: Each style offers three, and they are chosen to still work with that style's body face. Naming a role outright is the override path, and it is where a pairing can go wrong.

Engine. Every bundled family is declared in the generated stylesheet, and only the ones a rendered element resolves to are downloaded — see interface-guidelines.md, Typography, for why that has to be every set.

Illustrations#

A set of small drawings that carry a style's vocabulary, in four slots: A section mark, a divider, a mark inside the header, and an empty state.

  • None — no drawings. What Institutional, Newsroom, Zine, and Broadsheet ship.
  • Botanical — leaves, seed heads, specimen sprigs. Built for Field Guide, off by default.
  • Fauna — the same idea in animals.
  • Cartographic — compass points, contour fragments, route marks. On for Atlas.

They take the site's own ink and never carry a colour of their own, so a set cannot fight a client's palette.

Engine. config/theme.motifSet names the set; data-motif-set on the root element does the switching, because a custom property cannot rewrite the asset another custom property points at. A motif renders as a mask-image painted with the ink token, or as an inline SVG symbol reading currentColor — never as an <img>, never as a url() fill.

Layout#

The stage and the measure#

The frame and running text each have a width token. The frame defaults to 95% and centers. The measure defaults to 100% of that content box.

The stage is the frame. The header, the navigation, the schedule, the speaker shelf, the sponsor wall, the footer, and every section heading run to it. It leaves a small margin on both sides.

The measure is running text. Paragraphs, lists, rich text, descriptions and answers fill the stage's content box, matching the section rule.

The stage retains its --space-lg inline gutter for the Atlas coordinate mark. At viewports of 1024px and wider, its content box uses at least 90 percent of the viewport. The specimen split uses one full-width track, and folios stay inside the stage at the end of the rule.

Engine. Both widths are tokens: --stage-max and --measure-text, declared in the page contract over a tier 2 stage family. Their source is design/tokens/primitives.json; regenerate the committed CSS with node scripts/generate-content.cjs --demo. The arrangement variant maps onto them: grid uses the stage's columns and list uses the measure. Admin long-text fields fill their panel content box; short helper paragraphs, date fields and category controls retain their own limits.

Pages are chosen by task#

An operator knows "this is a long read" and "this is a directory with an introduction". They do not know, and should not have to work out, what arrangement should be. So the page editor asks the question they can answer:

TemplateWhat it is for
Standard pageThe ordinary page: A running header, one column, normal spacing. Start here.
Feature firstOpens on its main content under the full masthead, for a page whose subject is the first thing to see.
Directory with introductionA few words, then entries in columns. Speakers, sponsors, anything that is a list of people or organizations.
Long readOne column with air between things, for a page that is mostly text.
ScheduleDense and time-led, so a long day fits on one screen.
Landing pageThe full masthead over items in columns. A front page or a section opener.

Picking one sets every underlying value at once. The individual axes stay reachable behind a disclosure, for the page that genuinely needs to differ — and moving one clears the template, because a page that no longer matches its template should not go on claiming it.

A template is never inferred. A page whose values happen to match Long read has not chosen Long read; it has values that coincide. The editor reports absence as absence rather than reporting a decision nobody made.

The axes underneath (Advanced)#

  • Header style — the four above.
  • Arrangement — list or grid. Whether the page's items run down or across.
  • Density — tight, comfortable, loose. A set of token remaps, never raw values: Each step maps the page and session contracts onto the spacing scale, and the block nearest an element wins, which is how a page overrides the style for its own subtree and nowhere else.

Where the navigation sits is part of what the site is. A reader who meets a top navigation on the home page and a rail on the schedule has been handed two sites, and the navigation stops being furniture they can stop noticing. It is one choice — top or side — and it covers every page.

side moves the same navigation to the leading edge at wide viewports. It is one navigation either way: Same landmark, same items, same place in the document, and below the wide breakpoint it is the top navigation again.

Engine. The value lives in config/theme beside the style, the texture, the radius, and the mode policy. A page document's own stored value is still accepted on write and still read as that page's fallback, because deployments made before the move set it per page and an upgrade may not restyle their pages.

Sections are inserted, not slotted#

A system page — home, schedule, speakers, sponsors — keeps its built-in feature and lets you put sections around it. The control asks where relative to that feature: before the main feature or after the main feature.

Engine. The stored positions are above, main, and below. A section with no position is main. below is a third stored position with no third name in the editor: It renders after every main section, and a section stored that way keeps both its value and its place unless the operator moves the control. A custom page has no built-in feature, so it ignores position entirely.

Colour#

What a client actually sets#

One colour. The control is Main brand colour, and everything else follows from it:

  • The darker and lighter steps of it, for the states and the surfaces that need them.
  • The rules — hairline, strong, nameplate — which are the brand ink mixed into the ground at fixed shares, so a restyled site never keeps build-time rules on a new ground.
  • The form control boundary, mixed the same way but at a share high enough to clear the non-text contrast bar against either ground.
  • The dark palette, authored from the light one rather than inverted from it: A dark site is its own palette, and every colour is lifted to clear its bar on the dark ground rather than reused.

Semantic colours — success, warning, danger, and the rest — come from the style and are not part of the brand decision. They mean what they mean, and a client's colour does not get to redefine them.

Expert overrides#

Any single colour can be set by hand, per mode, under Advanced colour settings. A light tab and a dark tab hold separate values, and a blank field keeps the worked-out one. This is an exceptional control: It is there for the client whose brand guide names an exact value that the derivation does not reach, and every use of it takes a token out of the derivation permanently — it will no longer move when the brand colour moves. Most sites should never open it, and a site that has overridden a dozen values has usually picked the wrong style.

There is no second brand colour anywhere in the system. The admin's own mark takes the brand colour where it can be seen against the admin's white title band and falls back to the admin's ink where it cannot, so there is nothing to set and nothing to get wrong. The admin's rail, buttons and links follow the same colour by default, worked to the admin's own contrast bars in both modes; Admin colours on the Branding tab can replace that with a house scheme, and the public site never sees the choice.

The contrast gate#

Publishing is refused when any defined foreground/background pair fails its bar, in either mode. Text holds 4.5:1. A form control's boundary is non-text user interface and holds 3:1, measured against every ground an input actually renders on. Contrast is measured against the real rendered background, not the page background.

A failure stops a publish, not a save. A draft may hold two colours too close together to read; publishing that draft is refused, and the message names the pair, the mode, and the ratio it measured.

Modes#

light, dark, or system. Every style defines both palettes, so no choice here leaves the site half-dressed. The mode arrives as data-mode on the root element, written by the runtime; a deployment whose policy is not light also gets a first-paint block, so the ground is right before that attribute lands and a reader never sees a flash of the other mode.

Tokens#

Three tiers, and each one only reads the one below it.

TierNames look likeWhat it is
1 — primitives--er-space-md, --er-size-h2-maxRaw values. Nothing renders against these directly.
2 — semantic--space-md, --text-h2, --color-text-secondary-rgb, --rule-hairline-widthThe scales and roles components ask for.
3 — contracts--folio-size, --section-rule-width, --session-card-pad-blockOne device's settings, so a style retunes a device by remapping tokens rather than by changing a component.

Rules for using them, in full in interface-guidelines.md, Colors:

  • Name a token by role, never by appearance or first use.
  • Never borrow a token from another role. When a role changes colour, mint a new token.
  • Every colour token is defined under both modes. A token missing from either is a bug, not a polish item.
  • Rules read the rule tokens. A rule never borrows an ink step and never carries brand colour.
  • A form control's boundary reads the control token, never the hairline: A hairline is tuned for low-contrast structure and falls well short of the 3:1 bar.

Colours are stored as space-separated RGB triples, so the utility layer's rgb(var(--…) / <alpha-value>) keeps its opacity modifiers working.

Where each is written. design/tokens/*.json is the source. scripts/lib/tokens.cjs is the only thing in the repo that mints a token. Never hand-edit a generated stylesheet.

Composition rules#

The devices, and the one job each has. The implementations live in apps/web/src/components/editorial/, and each resolves through its tier-3 contract.

  • Header — the site identity, in the picked header style. Never a heading, never a photo behind the name.
  • Folio — a small-caps plain-text label on a hairline. Never a chip, never a pill, never a coloured badge, and never directly above a heading.
  • Rule — hairline, strong, or nameplate. A rule replaces a card border. Where the rule belongs to a row that already exists, put the border on that row instead of adding a node.
  • Section boundary — one strong rule, then the heading with the folio beside it. Reach for this rather than composing a folio and a heading by hand.
  • Stat block — four parts, all required: The finding in words, what the number counts and over what period, where it came from and when it was read, and a line describing it for a screen reader. A large number with a small caption under it is not a stat block, and the write path refuses one.
  • Definition list — term and description pairs in a real <dl>, ruled between pairs: Venue, dates, format, room. This is the answer to a non-numeric fact, and the fact block renders through it: A fact is a term and a description, never a stat block, so it names no source and counts nothing. The term's face, case and column width are the definition-list contract.
  • Timeline — dated entries in order, as an ordered list on a spine: Past editions, key dates, milestones. The spine is the one the updates feed draws, through the timeline contract, so the two are one thread of time. The date sits beside the entry's title in the mono face and is the entry's only number; nothing counts the entries and nothing is zero-padded.
  • Updates feed — a timeline with standing heads that can open with one Featured update under its own folio (a larger title, a dateline and a standfirst on the same spine, never a card), and an update's category is a plain tag beside its title, before Pinned.
  • Ruled table — rows and columns a reader compares, as a real <table> with row rules, tabular figures, and a horizontal scroll region at narrow widths that is the table's own scroll container, so the page never scrolls sideways and the head is read once rather than pinned. A sortable head holds a <button> that sorts and the <th> carries aria-sort. The three rule widths are the table contract, which the Table rules option remaps.
  • Progress — how much of a set of tasks is done: A native <progress> with the fraction stated beside it, "3 of 5 tasks done". <meter> is not used, because it measures a quantity in a range rather than completion. Never a ring. The track, the fill, the height and the corner are the progress contract.
  • Notice bar — a site-wide message with a level, as a ruled band under the header. role="status" for information and role="alert" for urgent; the level is a word and a rule weight, never a colour alone (Zine, whose plain rule is already strong, doubles it for urgent). A dismiss control, remembered per browser by the notice's own id and keyed to the id the bar is showing now, so a new notice in the same slot shows; focus moves to the main landmark after a dismissal. The notice-bar contract sets the ground, the rules and the faces.
  • Standfirst — the one sentence under a heading that says what the page or session is, at the lead step and never above the heading. Each style names its face and its rule through the standfirst contract.
  • Byline and dateline — who, in what role, and when, in the data face under the title. A dateline carries the event's clock, never the reader's. One byline contract sets both.
  • Long read opening — the first paragraph of a page on the Long read template takes the style's opening: A drop cap, a standfirst-sized line, or nothing. The Long read opening option picks one by remapping the long-read contract; a page whose values merely resemble the template gets none.
  • Count — a labelled figure beside the thing it counts, "12 saved": The figure in the mono face with tabular figures, the label always present in the data face, never a bubble. The figure sentence composes the same figures into a line of body copy in place of a tile.
  • Legend — one line at the head of a list, in the data face, naming what a mark or a count means.
  • Session state marker — "Running now", "Next", "Finished" as a word in the data face with an ink change and a rule under it. A live session takes the accent; a finished one takes the back-issue ink on the hairline. Never a pill.
  • Avatar — a person's picture, or their initial where there is none, square on the brand radius and never a circle. Initials in the heading face on the alternate ground, inside a hairline frame; the avatar contract sets the ground, the ink, the frame and the corner.
  • Pull quote — a quoted sentence with its attribution under it, one per page at most, and the page enforces it: the first quote block in reading order renders through this device and any later one is set as a plain quotation in the body (components/blocks/pullQuoteBudget.jsx). The sentence is the callout device, so Zine's handwritten line and every other style's ruled quote are one element; the frame around it (the rules above and below, the rule at the inline start, the marks, the alignment) is the pull-quote contract. Every style draws quotation marks, either the large opening mark or the inline pair, because the editor tells operators not to type them.
  • Grid schedule — time down the left, lettered tracks across the head, at wide viewports. A real table, and it degrades to the time-ordered list, which is the accessible baseline and not a lesser view.
  • Back issue — the archival state of a past day. Reduce the palette to the archive tokens, add the folio, remove the live controls. Never hide the content.
  • Print view — its own view, not the screen with the controls hidden.
  • Loading — a stated line saying what is loading, and a block of hairline rules that holds the space the content will take. No spinner, no shimmer, no skeleton, and nothing that moves.
  • Toast — a repeat of a result the page already states in place. Its tone is a word and a rule weight, never a colour on its own, and a repeat is silent so one result is announced once.

The shared controls#

Keyboard-first, on the token system, in apps/web/src/components/forms/. Every one carries the ten interaction states in interface-guidelines.md, Interaction states.

  • Switch — a setting that is on or off. role="switch", the state as a word beside the label, 44px on touch. The label names the enabled state. No sliding knob.
  • Segmented control — one choice from a short set, as one ruled row of words. The chosen word takes the filled ground and the bold weight. One tab stop; arrow keys, Home and End move inside it. Never a pill.
  • Tabs — one panel from a short set, on the ARIA tab pattern with a roving tab index. The selected tab carries the strong rule, the same device a section boundary uses. Never a pill, never a filled tab.
  • Checkbox and radio — drawn from the tokens rather than painted by the operating system: the boundary on the control token, the checked fill in the accent, the mark an inline drawing that reads the ink around it. The input under the paint is untouched, so the keyboard, the label and the group stay the browser's.
  • Search field — a labelled search input, a stated clear control, and a result count the page speaks as well as shows.
  • Sort control — a labelled select. The label names what is being ordered.
  • Filter group — a fieldset with a legend, the count of what is on inside the legend, and one clear control for the whole group.
  • Dropzone — send a file: A ruled region around a real file input, a stated progress line with a <progress> element, and a sent list with a state word per file, both in one role="status" region; the region carries aria-busy while files go. The region's rule is dashed at rest and solid at the strong width while a file is held over it (a style whose rest rule is already solid or strong widens it further, so the state is never the accent alone), it shows the focus ring while the input inside it has focus, and in error it takes the strong rule in the danger ink under the refusal's sentence. The dropzone contract sets the rules, the ground and the corner.
  • Repeater — a short list of rows a person adds and removes: Each row's fields labelled, one remove control per row that names its row, one add control under the list that stays at the cap with aria-disabled and the limit in its name, and focus that follows the change. Rows are ruled between through the repeater contract.
  • An unavailable tab or segment stays in its row with aria-disabled="true" rather than leaving it, so the arrow keys can land on it and a screen reader hears that it is unavailable and why (the hint is read as part of its name); a sighted reader sees a dashed rule under the word and the hint drawn under the row while the word has focus or the pointer; its handler refuses every activation path; the row's one tab stop follows focus and returns to the selected word when focus leaves. Tests prove all of it. A tab or a set with nothing behind it at all is not rendered.
  • Dialog — a native dialog opened as a modal, so focus is trapped, the page behind is inert, and Escape closes it. Focus returns to the control that opened it. The scrim is tinted ink, never a blur.
  • External link — any link that opens a new tab says so inside the link's own name.

The eyebrow ban is absolute. Nothing sits directly above a heading — not a label, not a chip, not a small line of description, not a plain folio. It holds at every size, in every style. Two things are not eyebrows and must never be "fixed": Metadata inside the rule-bounded nameplate block, and a form <label> above its own input.

Elevation is tint, not shadow. Where a surface must sit above another, shift its tone.

Flat surfaces are the shared default. The page ground paints no texture on its own; paper is a treatment a style opts into. An opt-in cannot leak.

The full list of rejected patterns is in interface-guidelines.md, Rejected patterns.

Print#

Paper has no dark mode. Print any schedule page from a dark screen and you get the light edition: The print rules read the ink and rule tokens and nothing else, so re-pointing those at the light palette is the whole switch, and no print rule has to know a mode exists.

The handout is its own view. Every day of the event, every session and every stop under it, tracks named by letter and by name, no buttons, no demo notice. Nothing has to be prepared for it.

The live palette prints, not the shipped one. There are two print blocks and they are not redundant. The generated stylesheet carries one whose values are frozen at build time — that is the fallback, for a reader with no JavaScript or one printing before config/theme arrives. The runtime element writes its own from the live resolved palette, naming the same selectors so it wins on document order. Without it, a site restyled after deployment printed the palette it shipped with, and only from a dark screen, which is exactly the kind of bug nobody finds until a client hands out a hundred programmes.

The admin panel is not a site style#

The admin CMS is its own design surface and it does not restyle. It reads its own fixed token set, obeys the mode, and ignores the site style entirely — so the room a staff member works in does not change shape every time a client changes their palette.

  • Two faces, fixed. Source Sans 3 for everything a person reads as language, IBM Plex Mono with tabular figures for everything the machine owns, on the admin's own type scale.
  • The frame is a dark rail against a light canvas. Navigation is a grouped standing list of words on the rail down the leading edge. The Overview stands above six groups with no folio, and the admin opens on it. Each group folds behind its name, and the group of the current page opens by itself. On a wide screen Hide menu narrows the rail to your logo and a Show menu button. No icon rail, no collapse to glyphs, no counts in bubbles. Your logo sits at the head of the rail on a small white tile, beside the event's short name.
  • The current item carries four signals, never colour alone: The marker, the weight, the filled ground, and the assistive-technology current-page state.
  • The page title carries the site's own brand colour, not a second colour anyone picks. There is no marker colour field: The small mark beside the page title takes the resolved brand colour for the mode. A mark is non-text interface, so it holds 3:1 against the white title band; below that it falls back to the admin's own ink and the editor says so. The site keeps painting the client's colour either way.
  • One action colour for the primary button, the current rail item, the links, and the focus ring. By default it is the site's main brand colour worked to the admin's own contrast bars, in light and in dark; Admin colours on the Branding tab can replace it with one of six house schemes: navy, graphite, forest, oxblood, teal, or plum. Either way it is one colour per deployment, and it does not change with the site style.
  • Three state words, everywhere: Draft, Live, and Live with unpublished changes, each set in a tinted badge.
  • A destructive action stands still and states what it costs. Nothing animates in a destructive moment.
  • The tour is a panel above the page. On a first visit a short tour stands at the top of the work surface, on the panel ground, with one step per group on the rail. There is no dimmed backdrop, so the page under it stays usable. Take the tour at the foot of the rail opens it again.
  • Unpublished changes are counted once. While something is saved and not published, a band on the proof ground above the page states the count in one sentence and links to the Unpublished changes page, which states the same sentence. The band and the page read one source, so they cannot disagree. The count is never a number on the rail.

Engine. The admin-* blocks are emitted once per mode and never once per (style, mode), which is the mechanical form of "the admin ignores the site style". They stay root-only, because the admin never renders inside the page-preview frame.

The specimen book#

Staff. You will not meet this page. It is a review surface for the people who build and extend the look.

Engine. /specimen renders every device the system has, in the site style and display mode the page is set to, with the component file and the tier 3 contract beside each one. It exists because a designer otherwise has to open real pages to see a device, and five of the six styles have devices no seeded page draws.

Thirteen sections, in this order: type, colour, rules and spacing, layout, headers, editorial devices, illustrations, sessions and schedule, directories, controls, inputs, feedback, print. Each specimen is a <figure>, and its <figcaption> names the device, the file, and the contract. The event content on the page — the name, the days, the rooms, the sessions, the speakers — comes from the committed synthetic snapshot in apps/web/src/generated/. The words inside a control example are written for the book, because a control needs a label, a hint and a refusal that the snapshot has no field for. A section's number is its place in the contents, not a string inside the file, so inserting one renumbers the book on its own.

  • Where it ships. The demo build and a development server, never a client production build. The gate is SPECIMEN_ENABLED in apps/web/src/pages/specimen/specimenRoute.js, written as an expression over two build constants so the bundler folds the route away and never emits the chunk.
  • What it refuses. The page marks itself noindex while it is mounted, and scripts/write-site-files.cjs fails the build if a sitemap ever lists the route.
  • What keeps it complete. tokens.js lists the scales the book draws and a test pins each list to design/tokens/semantic.json, so a new colour token, type step, page width, state share or focus-ring value fails until the book shows it. A second test renders the whole page and checks that every component under components/editorial/ and components/forms/ appears in it.
  • A device a style switches off. Three devices read a display token that four of the six styles hold at none: the plate number, the specimen label's term, and the pen mark. Each figure sets that one token to the value a style that uses the device sets, and says so in its caption.
  • Adding a control. The controls section is a registry: one file per control under apps/web/src/pages/specimen/controls/, plus one import and one line in index.js. The file declares which of the ten interaction states it draws and which it does not have, with a reason for each; a control that accounts for neither fails controls/index.test.js. The reasons are the point: a state that is missing looks the same as a state somebody decided against, and only one of those is a defect.
  • Captures. scripts/dev/capture-specimen.mjs writes one full-page PNG per style, mode, and width. See scripts/README.md.

Extension points#

What a developer can add, and what the system will refuse.

A new site style. Add its JSON under design/tokens/presets/, with both palettes, its type map, its shape, and its option groups. A style joins the catalog only when it is complete: It is built to that standard on its branch, it clears the contrast gate in both modes, and only then does it register — so nothing half-finished can reach the picker, and no registration carries a stability tier, because the catalog has none.

A new option inside a style. An option remaps tokens the style already declares. It may not introduce a property name and it may not introduce a class. If it needs a name that does not exist, the thing you are adding is a token, not an option. Every style now carries eight groups: The heading face, the header style and the schedule style, plus the five the vocabulary expansion added — the long read opening (longReadOpening), the quote device (quote), the directory style (directory), the section boundary (sectionBoundary) and the table rules (tableRules) — and Zine and Field Guide keep their pen marks. A choice may name a component face as well as tokens (componentFonts), which is how Zine's toner-block quote drops the script face. The catalog is split by who needs it: packages/shared/src/presetCatalog.cjs carries the palettes, the type maps, the shape and every group's default and choice ids, which every path reads, and packages/shared/src/presetRemaps.cjs carries what a style moves: its own token and component-font remaps, what each choice moves, and the component defaults a style change resets, which only a path that resolves a style at runtime loads. The public site's first paint needs none of the remaps, because the generated stylesheet already carries the chosen style with its picks resolved, so the web app fetches them as a lazy chunk (apps/web/src/lib/presetRemaps.js) when a live config/theme overlay, the demo's style switcher, the admin or the specimen book asks; a Node caller requires shared/presetRemaps once. The resolver refuses to resolve a style's tokens or picks until they are loaded, so a style is never half-resolved.

A new font set. Add the woff2 to the bundle, declare the family, and register the set. It becomes selectable everywhere at once, because the type map is live.

A new illustration set. Add it to the motif JSON with all four slots filled. A set with a hole in it renders nothing in that slot, which reads as a bug.

A new page template. A template is a named, validated bundle of layout values plus the record of which task the operator meant. Add both, and state every axis: A template that left one unanswered would leave the page following the style for that one value while stating the others, which is the half-configured state templates exist to prevent.

What a style may never do: Grant itself an exception to a rejected pattern, introduce a property name, ship a remote font, or define a colour in one mode only.

This page is generated from docs/design-reference.md. Edit the Markdown, not the HTML.