# Since Labs — brand guide, complete text Version 2.2.1. Generated 2026-08-28 from https://github.com/sincelabs/brand. Canonical: https://brand.sincelabs.com/ The marks described here are trademarks of Since Labs and their use is governed by https://brand.sincelabs.com/legal/. The guidance and tokens are published to be learned from; the marks are not published to be adopted, altered, or synthesised. --- # 01 · Foundation > Who Since Labs is, and what every other page in this guide is trying to protect. --- ## 1.1 The company **Since Labs** is an AI-native lab in Turku, Finland. It does two things that feed each other: | | What it is | What it looks like | |---|---|---| | **The Lab** | The consulting arm. We test every AI tool on our own company before it reaches a client's. | The AI Audit, AI Training, founder-led engagements | | **The Command Center** | The product. One layer where every agent a company runs is given memory, access and mission control. | `sincelabs.com` product surface; the Since Labs portal (`sincelabs/app`) | The lab proves what works. The command center is where what works gets run. That relationship is the brand's whole argument, and it is why the identity is *warm and editorial* rather than *cold and technical* — we are not selling software, we are selling the judgement of people who have already done the work. - **Stage:** proof-of-concept / pre-seed. Productising demand that was paid for manually first. - **HQ:** Turku, Finland. Launch market Finland → Nordics → EU core. - **Programs:** Boost Turku Startup Journey 2026; City of Turku PoC grant. - **Founding team:** Yuehan John (technical lead), Jaakko Mäkipelto (market, native Finnish), Talha Ahmad (sales, strategy). ### Positioning statement > Since Labs is the command center for AI agents — memory, access and mission > control in one place. And the AI-native lab that proves every tool on our own > company before it reaches yours. ### The origin, in one paragraph — use this, it is the strongest asset we have A client paid Since Labs to deploy AI agents by hand. Five more owners then asked for the same thing, unprompted. Everything the company has built since is the productisation of demand that already existed. **We are not solving a guessed-at problem.** Say this in pitches. Say it on stage. It is the difference between us and every company that started with a deck. --- ## 1.2 Who it is for Since Labs speaks to **owner-operators who are still doing the work by hand**. Not CTOs. Not innovation departments. The person whose evening disappears into paperwork. **Primary beachhead — Finnish infrastructure & earthmoving contractors.** Owner-operator (*toimitusjohtaja*) at maanrakennus / maansiirto firms, usually family-owned. 2–30 employees, €300k–€10M revenue, no in-house IT or admin. Tendering, invoicing and compliance handled between job sites. Price anchor €3k–€8k/year. - **Volume motion:** 2–15 employee firms, reached through Koneyrittäjät and *tilitoimisto* referral. - **Trophy motion:** larger, tech-forward firms as flagship references. - **Anti-ICP:** public sector, firms with an in-house IT/admin department, non-Finnish infra. **Secondary — the Lab's audit clients.** Non-technical owner-operators of service SMBs, 5–50 people, who need a plan written for their stack rather than a list of forty tools. ### What this means for design The audience reads on a phone, in a truck, in daylight, at the end of a long day. That is *why* the palette is warm and high-contrast paper rather than dark-mode neon, why the type is large, and why nothing important is ever communicated by colour alone. **The accessibility rules in this guide are not compliance theatre — they are the audience.** --- ## 1.3 Brand personality Since Labs sounds like **a sharp operator who has actually done the work** — not a hype merchant, not a corporate consultancy. **Three adjectives: Proven · Direct · Grounded.** | We are | We are not | |---|---| | Direct and specific | Vague and buzzwordy | | Proof-led — "6 hours a week" | Aspirational — "transform your business" | | Honest, including about failures | A highlight reel | | Practical | Theoretical | | Locally aware (Finnish market) | A generic US clone | | Builder-respectful | "AI will replace you" | ### The brand promise > We test every AI tool on our own company first. You get a plan written for your > specific stack — not a deck of forty tools and homework. --- ## 1.4 The design idea in one line **Warm paper, one hot accent, editorial type, and no decoration that isn't doing a job.** Every other AI company defaults to a dark cockpit with electric blue. Since Labs is cream, terracotta and a serif. That contrast *is* the differentiation — it is the visual form of "a person who has done the work, talking to you", and it is worth more than any feature list. Protect it. Three commitments fall out of that idea, and the rest of this guide is their consequences: 1. **Paper is the ground.** `#F7F5F2`, not white and not black. Roughly 60% of any surface. 2. **Terracotta is a scalpel.** `#C4602A` on ~10% of a surface. When everything is accented, nothing is. 3. **The serif carries the feeling, the sans carries the work.** Lora for what you are meant to *feel*; DM Sans for what you are meant to *do*; Geist Mono for what the machine said. --- ## 1.5 Brand architecture Since Labs is a **branded house**. There is one brand, and products are named plainly underneath it. ``` Since Labs ← the masterbrand. Always "Since Labs", never "SinceLabs". ├── The Lab ← the consulting arm (AI Audit, AI Training) └── The Command Center ← the platform ├── Sora 🏗️ ← Finnish municipal permit fees └── ← same pattern ``` **Rules** - Products get a **plain, short, human name** and an **emoji icon**, not a sub-logo. Sora is `🏗️`. This is deliberate: an app must be addable without a designer in the loop, and emoji survive at 16px in a launcher list where a bespoke glyph would not. - Products never get their own colour, typeface, or logo. They inherit everything. An app is a room in the house, not a house. - Never write "Since Labs Sora" — it is **"Sora, on Since Labs"**, or just **Sora** once context is established. - Product names are **not** capitalised in running prose beyond their own name: "the Sora register", not "the Sora Register". - The two internal contexts of the platform are **Workspace** (what customers see) and **Operator console** (what staff see). These are interface words, not brand names — lowercase them in prose. --- ## 1.6 Naming conventions | Thing | Correct | Wrong | |---|---|---| | The company | Since Labs | SinceLabs, Since labs, SINCE LABS, Since Labs Oy (unless legal) | | Possessive | Since Labs' | Since Lab's, Since Labs's | | The mark, spoken | "the double slash" | "the slashes", "the mark" | | Domain | sincelabs.com | since-labs.com | | Repo / package | `sincelabs/app`, `@sincelabs/brand` | `since-labs/app` | | Product | Sora | SORA, Sora™, Since Sora | | Old names | — | Since AI (retired), the marketplace (shelved 2026-07-25) | **Retired framing — do not reuse.** The two-sided "AI agent marketplace" and the "builder portal" were shelved on 2026-07-25. If you find them in a deck, a page or a repo, it is stale. Replace, don't perpetuate. --- ## 1.7 What would make this brand wrong A useful test. If a piece of work does any of these, it is off-brand no matter how good it looks: - It could be any AI company's. Swap the logo and nobody notices. - It promises a transformation instead of naming an outcome with a number on it. - It uses a glowing brain, a humanoid robot, or a circuit-board background. - It is dark, blue, and gradient — the default cockpit aesthetic we exist against. - It decorates. Every element in a Since Labs layout is load-bearing. - It is louder than the thing it is describing. --- *Sources: Since Labs Notion — `Agent.md` (2026-07-25 GTM correction), Brand Guidelines v1 (2026-06-03), Brand Design System Skills (2026-06-06); live `sincelabs.com`; `sincelabs/app` at `df8eb7e`.* --- # 02 · Verbal identity > How Since Labs sounds. This chapter governs everything with words in it — > landing pages, product microcopy, emails, decks, LinkedIn, sales replies, > error messages, commit messages people will read. --- ## 2.1 Voice principles **1. Lead with the concrete.** Numbers, outcomes, hours. "Four hours back every week", not "significant efficiency gains". If you cannot put a number on it, say the specific thing instead: "drafts the weekly report" beats "improves reporting". **2. Name the outcome and the agent — never the tooling.** "A custom agent that drafts the weekly report." We build custom agents. We do **not** name third-party automation tools (n8n, Make, Zapier, Clay …) in public content. Specificity about outcomes buys credibility; specificity about our plumbing gives it away. **3. Respect the reader's time.** Short sentences. No throat-clearing. Cut the first sentence of most drafts — it is usually warm-up. The reader is an owner-operator at the end of a long day. **4. Be honest, including about failures.** "Our first agent failed. Here's what broke." Nobody believes a highlight reel. The failures are what make the wins legible. **5. First person, founder voice.** "We tested", not "Since Labs conducted an evaluation". A named person talking to a named person. **6. Finnish-market aware.** Use local verticals and local words where they land — *tilitoimisto*, *maanrakennus*, *toimitusjohtaja*, Hilma, Koneyrittäjät. Default language is English; Finnish for Finland-market-facing material. Never machine-translate Finnish for public use — get Jaakko to read it. --- ## 2.2 The signature sentence pattern The brand's most recognisable verbal move opens with **"Since …"** and lands on the reader's own situation: - "Since you read about AI daily but nothing has changed yet." - "Since your team is still doing this by hand." - "Since every agent you run answers to one layer." - "Since we started, everything changed." **How it works:** the word "Since" does double duty — it is the company name and a subordinating conjunction, so the sentence is *already about the reader* before they reach the verb. Then a single phrase inside it carries the emphasis (serif italic, terracotta — see § 5.4). **Use sparingly.** One per surface. It is a hook, not a house style; a page that opens three sections with "Since …" reads as a gimmick. --- ## 2.3 Words **Words we use** > agent · workflow · deploy · reclaim (hours) · test / tested · validated · > stack · the gap · ship · automate · run · prove · by hand **Words we avoid** > revolutionary · cutting-edge · synergy · transform · unlock · game-changer · > "the future of work" · leverage (as a verb) · seamless · disrupt · robust · > best-in-class · empower · effortless · magic · AI-powered · third-party tool names **Words that need care** | Word | Rule | |---|---| | "AI" | Fine in positioning, weak in a value claim. "An agent that files your tender paperwork" > "AI-powered tendering". | | "Simply" / "just" | Delete. It tells someone their difficulty is their fault. | | "Obviously" / "of course" | Delete. Same reason. | | "Please" | Not in interface labels. Fine in an email to a person. | | "Sorry" | Only when Since Labs actually broke something. Not for a validation error. | --- ## 2.4 Tone by context | Context | Tone | |---|---| | Website | Clear, benefit-first, low-jargon. One idea per section. | | Product UI | Plain, calm, second person. Never chatty, never apologetic. | | Error / failure states | Say what happened, then what to do. No blame, no exclamation marks. | | Email (transactional) | Short, factual, one action. | | Email (sales) | Warm, direct, specific to *their* business. Name their vertical. | | LinkedIn | Confident, proof-led, ends with a real question. | | X | Sharper, punchier, opinionated. | | Substack | Reflective but substantive. One clear takeaway. | | Decks / grant applications | Evidence-first. Every claim gets a source or a number. | --- ## 2.5 Messaging framework ### Core value propositions 1. **Validated tools only** — "We don't recommend tools we haven't used. We test every AI tool on our own company first." 2. **Reclaimed time** — the hours counter. Numbers are the hero. 3. **No homework, just plans** — "A step-by-step implementation plan written for your specific stack." ### The case-study formula (use everywhere) ``` Problem: [Role] was manually [task] every [frequency]. Solution: We built a custom agent that [action] — zero manual input. Outcome: [X] hours reclaimed every [period]. ``` ### Elevator pitch (30 seconds) > Most companies know they should use AI but can't make it real — no developer, > no time. Since Labs runs every AI tool on our own business first, then deploys > what actually works into yours. You get a plan written for your exact stack, > and the hours back to prove it. ### Boilerplate (press, grants, footers) > **Short (1 line).** Since Labs is the command center for AI agents — memory, access and mission control in one place. > > **Medium (2 sentences).** Since Labs is an AI-native lab in Turku, Finland. It builds and runs custom AI agents for Nordic owner-operators, and gives every agent a company runs one place for memory, access and mission control. > > **Long (paragraph).** Since Labs is an AI-native lab based in Turku, Finland. We test every AI tool on our own company before it reaches a client's, then build and deploy the custom agents that take real work off owner-operators — starting with the tendering, invoicing and compliance paperwork burying Finnish infrastructure contractors. Everything we run answers to a single command center: one layer for memory, access and mission control across every agent in a business. --- ## 2.6 Product microcopy The rules that make interface text feel like the same company wrote it. ### Sentence case, always Headings, buttons, labels, table headers, menu items, page titles. **Never Title Case.** The only capitals are proper nouns and the ALL-CAPS eyebrow label. > ✅ `Request access` · `Recent access requests` · `Set your password` > ❌ `Request Access` · `Recent Access Requests` · `Set Your Password` ### Buttons name the action, not the concession The label completes the sentence *"I want to…"*. > ✅ `Save changes` · `Send invite` · `Delete organization` · `Book your AI Audit` > ❌ `OK` · `Submit` · `Yes` · `Learn more` · `Get in touch` · `Click here` ### Errors: what happened, then what to do Three parts, in order — and never more than two sentences. > ✅ "That invite link has expired. Ask your administrator to send a new one." > ✅ "Session expired — for your security, you've been signed out. Please sign in again." > ❌ "Error: token invalid." · "Oops! Something went wrong 😅" · "Invalid input." Never surface a stack trace, an error code, or the word "exception" to a customer. Operators may see IDs — set them in mono. ### Empty states do a job Title says what is not here. Description says why, or what to do. An action if there is one. Never a shrug. > ✅ "No apps are active for your organization yet." / "No access requests yet" > ❌ "Nothing to see here!" · "Empty" · "¯\\_(ツ)_/¯" ### Say what a thing is, not that it exists > ✅ "Everything your organization has access to. Pin the ones you use daily — they lead the ⌘K switcher." > ❌ "This is the apps page." ### Numbers, dates, units - **Numbers in tables and stats:** tabular figures, right-aligned. Always. - **Dates:** `28 Aug 2026` in prose; `2026-08-28` in operator tables and logs. Never `08/28/26` — it is ambiguous to a European reader. - **Relative time** for anything under a week: `3d ago`, `2h ago`, `Just now`. Absolute after that. - **Currency:** `€3,000`, `€3k–€8k/year`. Euro symbol leading, no space. - **Ranges:** en dash, no spaces — `2–30 employees`, `€300k–€10M`. - **Counts:** pluralise properly. `1 member` / `2 members`, never `1 member(s)`. ### Punctuation - No exclamation marks in the interface. One per marketing page, maximum, and only if it earns itself. - Em dashes — like this — with no spaces, for a break in thought. - The middle dot `·` is the brand's separator: `Apps · Since Labs`, `AI NATIVE LAB · SINCE LABS`. Not a pipe, not a slash. - Ellipsis `…` as one character, not three periods. Placeholders and in-progress labels: `Search apps…`, `Signing in…`. - Curly quotes in prose (`"…"`), straight quotes in code. - Serial comma: yes. ### Second person, active voice > ✅ "Your account is not attached to an organization yet." > ❌ "The account has not been attached to an organization." --- ## 2.7 Do and don't **Do** - ✅ Use real numbers, and name the agent and the outcome - ✅ Share failures and lessons, not just wins - ✅ Write in first-person founder voice - ✅ End social posts with a genuine question - ✅ Adapt local-angle content to Finnish - ✅ Cut the first sentence **Don't** - ❌ Use AI hype language or empty superlatives - ❌ Name third-party automation tools in public content - ❌ Use glowing-brain or humanoid-robot imagery - ❌ Promise outcomes we can't back with a real example - ❌ Sound like a corporate consultancy - ❌ Use Title Case --- # 03 · Logo and mark > The `//`, the wordmark, and the rules that keep them the same shape wherever > they land. Every file is downloadable from the [downloads page](/downloads/); > using them is governed by the [brand asset terms](/legal/). --- ## 3.1 The double slash The Since Labs identity mark is **`//`** — two forward slashes, italic, set in terracotta. It is doing three jobs at once, which is why it works: 1. **A comment marker.** `//` opens a comment in every C-family language. The mark says *written by people who write code* without a single circuit board. 2. **A forward slash, twice.** Motion, and a directory path — the command center is a place things live under. 3. **A wordmark abbreviation.** At 16px in a browser tab, `//` is still legible where "Since Labs" is a smudge. The mark is **never explained on-surface**. No tagline under it, no "the double slash represents…". It earns its meaning by repetition. --- ## 3.2 The asset set Everything below is generated from the vendor masters in [`../assets/logo/source/`](../assets/logo/source/) by `npm run assets`, which measures each master's real ink and crops the art board away. **Every file is trimmed to its own artwork.** Align to the edge of the image and add the clear space in § 3.3 — there is no hidden padding to compensate for. ### Lockups | File | What it is | Use | |---|---|---| | `since-wordmark.svg` | `//` + "Since Labs", one line, ink text | The primary lockup, light grounds | | `since-wordmark-on-ink.svg` | Same, paper text | Dark grounds | | `since-wordmark-adaptive.svg` | Same, inverts with `prefers-color-scheme` | Grounds we do not control — the repo README, npm, editor previews | | `since-wordmark-mono-ink.svg` | All ink | One-colour print, embossing | | `since-wordmark-mono-paper.svg` | All paper | One-colour on dark | | `since-wordmark-stacked.svg` | `//` beside "Since" over "Labs" | Space closer to square than to a strip | | `since-wordmark-stacked-on-ink.svg` | Same, paper text | Dark grounds | | `since-wordmark-stacked-adaptive.svg` | Same, self-inverting | Unknown grounds | | `since-wordmark-stacked-mono-ink.svg`, `-mono-paper.svg` | One-colour | Print | ### Mark and tiles | File | What it is | Use | |---|---|---| | `since-mark.svg` | Terracotta `//`, transparent | Default mark on any light surface | | `since-mark-ink.svg` | Ink `//` | Monochrome, one-colour print, embossing | | `since-mark-paper.svg` | Paper `//` | Monochrome on dark or photographic grounds | | `since-mark-currentcolor.svg` | `stroke="currentColor"` | Inline in code — inherits the text colour | | `since-tile-ink.svg` | Terracotta `//` on an ink rounded square | Avatars, app icons, favicons at ≥32px, social profile images | | `since-tile-cream.svg` | Terracotta `//` on cream | Light-ground avatar | | `since-tile-terracotta.svg` | Paper `//` on terracotta | High-emphasis app icon, sticker, merch | | `favicon.svg` | The bare mark | Browser tab | ### Renders `assets/logo/png/` carries transparent PNGs of every lockup (800 / 1600 / 3200 px wide, 600 / 1200 / 2400 for the stacked one), the mark (256 / 512 / 1024) and the ink tile at 180, 192, 512 and 1024 — the sizes iOS, Android and social profiles ask for. `png/since-social-card.png` is the 1200 × 630 Open Graph card. `since-labs-logo-kit.zip` is all of it, plus the terms, in one download. ### ✅ The wordmark is outlined The lockups are **outlined paths, not live text**. They render identically with no fonts installed — in Canva, in a PDF, on a printer's RIP. This supersedes the old DM Sans `` files, which needed the font present and fell back silently to a system serif when it was not. The wordmark's letterforms are a **serif**, not DM Sans. That is the artwork. Do not rebuild the lockup by typing "Since Labs" in DM Sans beside the mark; § 3.6 covers this. ### The mark, twice The distributable `since-mark.svg` and the inline `components/brand/Mark.tsx` are the same geometry expressed two ways: two round-capped strokes on a 34 × 60 box, fitted to the vendor master and re-checked against it on every asset build. The component exists because the nav needs the mark to inherit `currentColor`, which a file behind an `` cannot do. If the mark changes, both change. --- ## 3.3 Construction and clear space The mark is drawn on a **34 × 60 box**: two strokes 6.07 units across, sheared 0.18 units horizontally for every unit of drop, with round caps struck at the stroke's half-width. It is taller than it is wide — close to 17:30 — which is why it never sits in a square without a tile around it. **Clear space = the height of one slash stem (¼ of the mark's height) on every side.** Nothing enters that box — no text, no rule, no image edge, no other logo. ``` ┌─────────────────────┐ │ clear space │ clear space = 0.25 × mark height │ ┌─────────────┐ │ │ │ / / │ │ ← the mark │ │ / / │ │ │ └─────────────┘ │ │ │ └─────────────────────┘ ``` For the **wordmark**, the gap between the mark and the "S" of "Since" is fixed at one slash-stem width. Do not re-space it by eye. ### Minimum sizes | Asset | Screen | Print | |---|---|---| | Mark alone | 16px tall | 5mm | | Tile | 24px | 8mm | | Wordmark | 96px wide | 25mm | Below the wordmark minimum, **drop to the mark**. Never shrink the lockup until "Labs" closes up. --- ## 3.4 Placement - **Top-left** in an app shell or a document. **Centred** only on an auth screen, a cover, or a sticker. - On the dark auth panel the lockup sits top-left with a 48px margin — see `AuthLayout`. - One logo per surface. A slide does not need it on every page; a footer credit is enough. - Against a photograph: use the tile, never the bare mark. A bare `//` on a busy image is unreadable. --- ## 3.5 Colour rules | Ground | Mark | Wordmark text | |---|---|---| | Paper `#F7F5F2` / white | Terracotta `#C4602A` | Ink `#1A1917` | | Ink `#1A1917` / dark | Terracotta `#C4602A` *or* lift `#E07A38` | On-ink `#F5F1EA` | | Cream `#F0E4DA` | Terracotta | Ink | | Terracotta `#C4602A` | Paper `#F5F1EA` | Paper | | Photograph | Use a tile | — | | One-colour print | All ink, or all paper | Same | On the dark auth panel the mark is rendered at **80% opacity** so it recedes behind the wordmark — this is intentional and is the one sanctioned opacity change. ### Grounds we do not control A GitHub README, an npm page or an editor preview picks its own background, and most people read them dark. A transparent ink wordmark disappears there. Use `since-wordmark-adaptive.svg`: it carries a `prefers-color-scheme` rule and serves the light row of the table above by default, the ink row on dark — mark in `#E07A38`, text in `#F5F1EA`, both already sanctioned. Everywhere the ground is known, keep using the deterministic files. --- ## 3.6 Misuse Never: - ❌ Rotate, skew, or change the slash angle - ❌ Change the gap between the slashes, or between mark and wordmark - ❌ Recolour to anything outside § 3.5 — no blue, no purple, no gradient, no rainbow - ❌ Apply a drop shadow, glow, bevel, or outline - ❌ Re-set the wordmark in any typeface — the lockup is outlines, not text - ❌ Use Title Case variants — "Since labs", "SINCE LABS", "SinceLabs" - ❌ Add a tagline, a ™, or a strapline inside the clear space - ❌ Place on a busy photo without a tile - ❌ Stretch, condense, or scale non-uniformly - ❌ Reconstruct it by typing `//` in a different font and calling it the logo - ❌ Generate, upscale, restyle or "redraw" it with a model — see [the terms](/legal/) § 5 --- ## 3.7 The `//` as a typographic device Distinct from the logo: `//` is also a **texture** the brand uses in running material. Set in **Geist Mono, terracotta**, it can: - prefix an eyebrow label — `// AI NATIVE LAB` - open a section in a deck - mark a pull-quote or a stat callout - act as a bullet in a list of proof points This is not the logo, so clear space does not apply. But keep it to **one per surface** — the device stops meaning anything if it is decoration. --- ## 3.8 Favicon and app icons | Context | Asset | Notes | |---|---|---| | Browser tab | `favicon.svg` | Bare terracotta mark, transparent | | iOS / Android home screen | `png/since-tile-ink-180.png`, `-192`, `-512` | Tile — a bare mark disappears in the OS's own rounded square | | Social profile (LinkedIn, X) | `png/since-tile-ink-512.png` | Generous padding, never tight to the edge | | Slack / Notion workspace icon | `since-tile-terracotta.svg` | Reads at 20px in a sidebar | **Product apps do not get bespoke icons.** They get an emoji (Sora is `🏗️`) on a `surface-mist` or `accent-soft` rounded tile. See `01-foundation.md` § 1.5. --- # 04 · Colour > Warm paper, one hot accent, and a strict rule about which terracotta you are > allowed to read. Live swatches and every computed contrast ratio are in the > [colour explorer](/color/). --- ## 4.1 The idea Since Labs is **cream, ink and terracotta**. Not dark-mode navy, not electric blue, not a gradient. The palette is the single fastest signal that this is not another AI cockpit — protect it harder than anything else in this guide. The proportions matter as much as the values. **60 / 30 / 10:** | Share | Role | Colour | |---|---|---| | ~60% | The ground | Paper `#F7F5F2` | | ~30% | Content and structure | Ink, white surfaces, hairlines | | ~10% | Emphasis | Terracotta `#C4602A` | If a surface is more than roughly a tenth terracotta, it has stopped meaning anything. The accent is a scalpel. --- ## 4.2 The palette ### Ink — everything you read | Token | Hex | Use | |---|---|---| | `ink` | `#1A1917` | Headlines, primary text, dark surfaces, the dark-theme ground | | `ink-body` | `#2C2A28` | Running body copy. Softer than `ink` so long text does not vibrate | | `ink-muted` | `#6B6762` | Labels, captions, secondary text, inactive nav | | `on-ink` | `#F5F1EA` | Text on `ink`. **Warm off-white, never `#FFFFFF`** — pure white on a warm dark ground reads clinical and slightly blue | ### Paper — the grounds | Token | Hex | Use | |---|---|---| | `paper` | `#F7F5F2` | The default page background | | `paper-base` | `#F4F3F1` | Recessed chrome — sidebars, rails, the app frame. One step behind `paper` | ### Surfaces — things on top of paper | Token | Hex | Use | |---|---|---| | `surface` | `#FFFFFF` | Cards, panels, inputs, modals | | `surface-mist` | `#EFEFEC` | Subtle neutral fill — hover, neutral chips, icon tiles, avatar wells | | `surface-cream` | `#F0E4DA` | Warm accent fill. The tinted sibling of mist | ### Borders | Token | Hex | Use | |---|---|---| | `border` | `#E5E2DE` | Default hairline — card edges, table rules, dividers | | `border-strong` | `#D8D5D0` | Emphasised edge — input borders, secondary buttons | --- ## 4.3 Terracotta, and the one rule that matters This is the most important paragraph in the chapter. > **`#C4602A` is a fill colour. `#A34A1F` is a text colour.** `#C4602A` on white measures **4.16:1**. WCAG AA for normal text is **4.5:1**. So the brand orange, used as a link or a button label, *fails* — by a margin small enough that nobody notices in review and large enough to matter to a contractor reading a phone in daylight. The fix is not to abandon the brand colour; it is to keep a second, darker terracotta for anything with letterforms in it. | Token | Hex | On white | Use | |---|---|---|---| | `accent` | `#C4602A` | 4.16:1 | **Fills only.** The `//` mark, focus rings, chart series, dots, icons, accent tiles | | `accent-hover` | `#A84E22` | 5.56:1 | Hover for a terracotta fill | | `accent-ink` | `#A34A1F` | **5.90:1** | **Text and links** on light surfaces, and solid buttons whose label must clear AA | | `accent-ink-hover` | `#8B3D18` | 8.1:1 | Hover for accent text and links | | `accent-lift` | `#E07A38` | — (5.86:1 on ink) | Terracotta **on dark surfaces**. `#C4602A` only reaches 4.23:1 there | | `accent-soft` | `rgba(196,96,42,0.12)` | — | Translucent wash — active nav rows, attention chips | | `accent-light` | `#F0E4DA` | — | Opaque equivalent of `accent-soft` | Both `#A34A1F` and `#C4602A` already ship on `sincelabs.com` as `--accent-ink` and `--accent`. This is not a new colour; it is an existing one being given its job in writing. ### ⚠️ Conflicting values in circulation Three different "brand oranges" exist in Since Labs documents. **`#C4602A` is correct** — it is what both `sincelabs.com` and `sincelabs/app` actually ship. | Value | Where it appears | Verdict | |---|---|---| | **`#C4602A`** | Live site CSS (`--accent`), app `globals.css` | ✅ **Canonical** | | `#E8501A` | Notion `Agent.md` § 1 | ❌ Stale. Update `Agent.md` | | `#C4561A` | Notion "Brand Design System Skills" | ❌ An eyeball approximation of the live value | | `#A95022` | App dashboard charts, hard-coded | ❌ A drift. Close to `accent-ink` — use `#A34A1F` | | `#FF5C35` | Brand Guidelines v1 § 5 | ❌ An explicit placeholder, marked TO CONFIRM. Now confirmed | --- ## 4.4 Status colours Six states. Each is a **triple** — a background, a text colour, and a dot. | State | Background | Text | Dot | Means | |---|---|---|---|---| | Success | `#EAF3DE` | `#3B6D11` | `#639922` | Active, approved, live, complete | | Attention | `#F0E4DA` | `#9A4A1F` | `#C4602A` | Pending, waiting, needs review | | Warning | `#FFF3E0` | `#8A4A05` | `#B25C00` | Degraded, stale, partial | | Error | `#FCEBEB` | `#791F1F` | `#E24B4A` | Rejected, failed, disabled, suspended | | Info | `#EBF0F7` | `#3A4A60` | `#3A4A60` | Internal, draft, informational | | Neutral | `#EFEFEC` | `#6B6762` | `#6B6762` | Inactive, retired, archived, unknown | Every pairing above clears AA (5.4:1 to 9.0:1). **The dot is not decoration.** It is the redundant, non-colour channel. A status communicated by colour alone fails WCAG 1.4.1 and fails a colour-blind operator scanning a review queue. Ship the dot, or ship a word. Attention deliberately shares terracotta. "Waiting on you" is the brand's most common state, and it should feel like the brand rather than like a warning. ### On dark Every status has a second triple. Without it the light fills survive into the dark theme and a chip renders cream-on-cream — unreadable, and the exact bug that shipped in the scrap-desk mockup before this table existed. | State | Background | Text | Dot | Text on bg | |---|---|---|---|---| | Success | `#26321C` | `#83B456` | `#639922` | 5.6:1 | | Attention | `#40291E` | `#EF8D5D` | `#C4602A` | 5.6:1 | | Warning | `#3F2A1B` | `#E89150` | `#B7600B` | 5.5:1 | | Error | `#422725` | `#F3867F` | `#E24B4A` | 5.5:1 | | Info | `#292F36` | `#95A7BF` | `#677890` | 5.5:1 | | Neutral | `#2F2E2C` | `#A9A5A0` | `#7A7670` | 5.5:1 | > **A dark fill sits *above* the surface, not below it.** > This is the one thing that is not obvious. On light, a status fill is a pale > tint *lighter* than the page and darker than nothing. The instinct on dark is > to make it a deep tint — but a fill darker than the `#232120` card it sits on > is invisible: it reads as a hole, not a chip. Every fill above is a tint > *lighter* than the card, separating at 1.18:1 — the same margin the light set > gets against white (~1.15:1). `scripts/check-tokens.mjs` enforces both halves: AA for the label on its own fill, and that the fill is actually distinguishable from `dark.surface`. --- ## 4.5 Dark The brand **inverts**; it does not switch palettes. Ink becomes the ground, paper becomes the text, and terracotta lifts so it still clears AA. | Token | Light | Dark | |---|---|---| | Ground | `#F7F5F2` | `#1A1917` | | Surface | `#FFFFFF` | `#232120` | | Raised surface | `#EFEFEC` | `#2C2A28` | | Primary text | `#1A1917` | `#F5F1EA` | | Muted text | `#6B6762` | `#98938C` | | Border | `#E5E2DE` | `#38352F` | | Accent | `#C4602A` | `#E07A38` | | Accent wash | `rgba(196,96,42,.12)` | `#2E1E0F` | **Three traps.** 1. Muted text on dark must be `#98938C` (5.8:1). The site currently also defines `#7A7570`, which is **3.9:1** — fine for a 20px label, a failure for a caption. 2. Terracotta must lift to `#E07A38` on dark. `#C4602A` drops to 4.23:1 against ink. 3. **The table above is not the whole palette.** Status and chart colours invert too, and they are easy to forget because nothing on a light screen shows you they are missing. Their dark values are in [4.4](#44-status-colours) and [4.6](#46-data-visualisation). `accent wash` on dark (`#2E1E0F`) is tuned to sit on the **ground**, not on a card — it separates from `#1A1917` at 1.10:1 but from `#232120` at only 1.00:1. For an accent-tinted region *inside* a card, use the attention fill `#40291E`. `sincelabs.com` ships dark. **`sincelabs/app` does not** — it is light-only, and that is a deliberate, documented gap, not an oversight. See the [app audit](/audit/). --- ## 4.6 Data visualisation **Sequential** — one measure across ranked categories. Darkest is largest. `#A34A1F` → `#C4602A` → `#D27C53` → `#E9A88A` → `#F4D2C3` **Categorical** — unordered series. Terracotta always leads: it is the subject, the rest are context. `#C4602A` terracotta · `#3A4A60` slate · `#639922` moss · `#7A4A5F` wine · `#2C2A28` ink The categorical set is built to two constraints, both enforced by `npm run check`: 1. **Every entry clears 3:1 against white**, so a thin line or a small dot is a visible graphical object (WCAG 1.4.11). 2. **Every *pair* differs by at least 1.2:1**, so the set survives greyscale — a printed report, a projector, a colour-blind reader. That second rule is why the obvious warm choices are not in the set. `#B25C00` amber sits at 4.75:1 and terracotta at 4.16:1 — a 1.14:1 separation, which is two indistinguishable oranges the moment colour is removed. ### On dark The light set cannot carry over. Slot 5 is near-black ink, which is invisible on an ink ground — and `slate` at `#3A4A60` is a dark navy that all but disappears on a `#232120` card. **Sequential** — `#B0562C` → `#C4602A` → `#D27C53` → `#E9A88A` → `#F4D2C3` **Categorical** — `#C7632D` terracotta · `#6F8199` slate · `#639922` moss · `#9D5E7A` wine · `#A6A4A2` ink | Alias | Light | Dark | |---|---|---| | slate | `#3A4A60` | `#6F8199` | | moss | `#639922` | `#639922` | | amber | `#B25C00` | `#C0691B` | | wine | `#7A4A5F` | `#9D5E7A` | Slots 4 and 5 are **re-stepped, not merely lifted.** Lifting near-black ink far enough to clear the dark surface lands it on top of wine at ΔE 7 — indistinguishable even with full colour vision, before you get to colour blindness. Re-stepped, the adjacent pair clears ΔE 18. Every dark slot clears 4:1 on `dark.surface`. Rules: - **Five series maximum.** Past five, the reader is decoding a legend instead of reading a chart. Group the tail into "Other". - **Never encode by colour alone.** Label the series directly, or give each a shape or a pattern. - **Never a dual axis.** Two measures on two y-scales invent a correlation the data does not contain. Two charts, small multiples, or index both to a common base. - Gridlines are `border` `#E5E2DE` (`#38352F` on dark), never a mid grey. - Axis labels are `ink-muted` at 11px. - Area fills are the series colour at **12–15% opacity**, and the stroke is the full-strength colour. --- ## 4.7 Do and don't **Do** - ✅ Let paper dominate. Whitespace is the design. - ✅ Use `accent-ink` for anything with letterforms. - ✅ Pair every status colour with a dot or a word. - ✅ Use `on-ink` `#F5F1EA` on dark grounds, not `#FFFFFF`. - ✅ Check a new surface in **both** themes before calling it done — a light-only review cannot see a missing dark token. **Don't** - ❌ Use `accent` `#C4602A` as a text or link colour on light - ❌ Introduce a colour that is not in this chapter, "just for this one screen" - ❌ Ship a status chip or chart series without its dark value — the light fill survives into dark and renders cream-on-cream - ❌ Reach for Tailwind's default palette — `zinc`, `red-700`, `indigo-800` are all off-brand, and all currently in the app - ❌ Use gradients, except the one sanctioned accent-tile wash - ❌ Use pure `#000000` or pure `#FFFFFF` as a page ground - ❌ Communicate state by colour alone --- # 05 · Typography > Three faces, three jobs. Lora for what you are meant to feel, DM Sans for what > you are meant to do, Geist Mono for what the machine said. --- ## 5.1 The three faces | Face | Role | Where | |---|---|---| | **Lora** | Display | Page titles, hero lines, section headings. Nothing else | | **DM Sans** | Interface and body | Everything that is not a heading and not machine data | | **Geist Mono** | Machine data | IDs, emails, slugs, keys, timestamps, shortcuts, the `//` device | All three are on Google Fonts, free, and already loaded by both `sincelabs.com` and `sincelabs/app`. Load them with `next/font/google`: ```ts import { Lora, DM_Sans, Geist_Mono } from "next/font/google"; const lora = Lora({ variable: "--font-lora", subsets: ["latin"], display: "swap" }); const dmSans = DM_Sans({ variable: "--font-dm-sans", subsets: ["latin"], display: "swap" }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], display: "swap" }); ``` **Fallbacks**, for email and anywhere a webfont will not load: | Brand face | Fallback stack | |---|---| | Lora | `Georgia, 'Times New Roman', serif` | | DM Sans | `system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif` | | Geist Mono | `ui-monospace, SFMono-Regular, Menlo, Consolas, monospace` | ### ⚠️ Cormorant Garamond and Inter are not the brand faces The Notion "Brand Design System Skills" doc names Cormorant Garamond and Inter. That was written from a visual read of the site, before the token files were inspected. The shipped CSS on both properties is **Lora, DM Sans and Geist Mono**. Update the Canva brand kit to match. --- ## 5.2 Why a serif at all Because everyone else uses Inter, and because it is the fastest way to say *this was written by a person, not generated by a platform*. The serif is the brand's warmth, and it is the reason the identity survives being screenshotted next to a competitor. **Use it sparingly.** Lora appears on page titles and hero lines, and nowhere else. A serif paragraph or a serif button label reads as a magazine, not as software. The discipline is what keeps the serif meaning something. --- ## 5.3 The scale | Name | Size | Line | Tracking | Face | Use | |---|---|---|---|---|---| | Hero | 42px | 1.1 | −0.02em | Lora | Marketing hero. One per page | | Title | 28px | 1.15 | −0.7px | Lora | Page title (h1). The `PageHeader` size | | Section | 20px | 1.3 | −0.01em | Lora | Major section heading | | Panel | 15px | 1.4 | 0 | DM Sans 600 | Panel and card headings | | Body | 14px | 1.6 | 0 | DM Sans 400 | Prose, form values, buttons | | UI | 13.5px | 1.5 | 0 | DM Sans 400 | Nav rows, table cells, list items | | Secondary | 12.5px | 1.5 | 0 | DM Sans 400 | Captions, hints, metadata | | Eyebrow | 12px | 1.4 | **+0.08em** | DM Sans 500 caps | ALL-CAPS section labels | | Micro | 11px | 1.4 | 0 | DM Sans 500 | Chips, badges, keyboard hints | **11px is the floor.** Nothing in a Since Labs interface is smaller, ever. **Weights:** 400 regular, 500 medium, 600 semibold, 700 bold (wordmark only). There is no light weight and no black weight in this brand. **Tracking:** negative on display sizes (large type sets loose), zero on interface text, and **+0.08em on the eyebrow only**. Letter-spaced body copy is not a Since Labs thing. --- ## 5.4 The italic move The brand's single most distinctive typographic gesture: > **Serif italic, in terracotta, on the one phrase that matters.** ``` Since we started, everything changed. ``` Rules, and they are strict: - **One per surface.** A hero line, or a pull quote. Not both. - **Never in body copy.** It loses all its power the second time it appears. - Colour it `accent-ink` `#A34A1F`, not `accent` — it is text. - The italic phrase must be the thing you would want someone to remember if they read nothing else. --- ## 5.5 The mono rule Geist Mono means **"a machine said this"**. It is not a decorative face and it is not for emphasis. **Set in mono:** - Identifiers — user IDs, org IDs, product keys, slugs - Email addresses in operator tables - Timestamps and dates in logs - Audit-log action names, HTTP status, error codes - Keyboard hints — `⌘K`, `esc`, `↵` - Counts in a nav badge - The `//` device **Never in mono:** - Body copy, headings, button labels - A person's name - A number that is a *quantity* rather than an *identifier* — `147 members` is DM Sans with `tabular-nums`, not mono This distinction is load-bearing. It is how an operator scanning a table knows, without reading, which column is a thing they can copy into a support ticket. --- ## 5.6 Rules that hold everywhere **Sentence case.** Headings, buttons, labels, table headers, menu items. The eyebrow is the only uppercase in the system. **Measure.** Prose caps at **62ch**. Captions and helper text at **46ch**. Past that, a reader loses the line return. **Hierarchy comes from size and weight, not colour.** A heading is a heading because it is bigger, not because it is orange. **One h1 per page**, and it is the `PageHeader` title. Do not skip levels — h1 → h2 → h3, never h1 → h3. **Numbers in columns are `tabular-nums`.** Always. Proportional figures in a table make a column of amounts impossible to compare at a glance. **Emphasis is `` in `ink`,** not colour, not italic, not caps. --- ## 5.7 Do and don't **Do** - ✅ Use Lora for titles and hero lines only - ✅ Keep the italic-terracotta move to one phrase per surface - ✅ Set identifiers in mono and quantities in tabular sans - ✅ Cap prose at 62ch **Don't** - ❌ Set body copy in Lora - ❌ Use Title Case - ❌ Letter-space anything but the eyebrow - ❌ Go below 11px - ❌ Introduce a fourth typeface — including Inter, Geist Sans, or Cormorant - ❌ Use colour as the only signal that text is a heading --- # 06 · Layout and form > Space, grid, radius, elevation. The quiet decisions that make two screens > built by two people look like one product. --- ## 6.1 Space A **4px base scale**. Everything lands on it. `2 · 4 · 6 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 56 · 72` | Gap | Between | |---|---| | 4–6px | Icon and its label; a dot and its chip text | | 8–12px | Related controls in a row; form field and its label | | 16px | Cards in a grid; fields in a form | | 20–24px | Panel padding; card padding | | 32px | Page gutter; between major sections | | 40–56px | Between page regions | **Whitespace is the design.** The brand's warmth comes from the paper being allowed to show. When a layout feels wrong, the answer is almost always more space, not more border. --- ## 6.2 Containers | Container | Width | Use | |---|---|---| | Page | `max-width: 1280px`, `padding: 32px` | Every product page | | Reading | `max-width: 768px` | Single-column form or prose pages — account, settings | | Prose | `max-width: 62ch` | A block of running text inside any page | | Auth card | `max-width: 440px` | Sign-in, set password, invite | | Command palette | `max-width: 520px` | ⌘K | ```tsx
``` The page container is centred, never full-bleed. A table that needs more room scrolls inside its own box — it does not widen the page. --- ## 6.3 The app frame ``` ┌──────────┬────────────────────────────────────────┐ │ │ │ │ Sidebar │ Main — scrolls independently │ │ 264px │ bg: paper #F7F5F2 │ │ bg: base │ │ │ #F4F3F1 │ ┌──────────────────────────────────┐ │ │ │ │ max-w-1280 · px-8 py-8 │ │ │ ── nav │ │ PageHeader │ │ │ │ │ content │ │ │ ── user │ │ │ │ └──────────┴──┴──────────────────────────────────┴──┘ ``` - Sidebar is **264px**, `paper-base`, with a `border` right edge. - Below `lg` (1024px) it becomes a drawer behind a 56px top bar. - The main column scrolls; the sidebar does not. - The sidebar's own footer holds the account block and sign-out — never in the nav list. --- ## 6.4 Radius Radius encodes **scale**: the bigger the surface, the rounder the corner. | Token | Value | Applies to | |---|---|---| | `xs` | 5px | Keyboard hints, tiny tags | | `sm` | 7px | Inline inputs, compact fields | | `md` | 9px | Nav rows, icon buttons, avatar tiles — the interface default | | `lg` | 12px | Buttons, dropdowns | | `xl` | 14px | App cards, dashboard tiles | | `2xl` | 16px | Panels, modals, primary cards | | `full` | 9999px | Status chips, dots, avatars, pill CTAs | Nothing is square. Nothing is rounder than 16px except a pill. --- ## 6.5 Elevation **Surfaces are separated by a border, not by floating.** This is the single biggest reason the product reads as calm. | Level | Shadow | Use | |---|---|---| | Flat | none | The default. Most things | | Raised | `0 1px 2px rgba(26,25,23,0.04)` | Cards and panels. A hint of a seam | | Lifted | `0 2px 8px rgba(26,25,23,0.06)` | Hover on an interactive card | | Overlay | `0 16px 48px rgba(26,25,23,0.16)` | Modals, ⌘K, popovers. The only real shadow | Shadows are **warm-tinted** — `rgba(26,25,23,…)`, the ink, never `rgba(0,0,0,…)`. A neutral-black shadow on a cream ground goes grey and dirty. **Order of preference for separating two things:** space → a hairline → a background change → a shadow. Reach for the shadow last. --- ## 6.6 Borders - **1px, always.** There is no 2px border in this system except the focus ring and the `Alert` left edge. - `border` `#E5E2DE` for structure — card edges, table rules, dividers. - `border-strong` `#D8D5D0` for anything interactive — inputs, secondary buttons. - Dashed `border-strong` for an empty drop target or a placeholder region. Nowhere else. **Note:** neither border colour reaches 3:1 against white (1.29:1 and 1.46:1). That is acceptable for *decorative* separation, but a border must never be the **only** thing identifying a control. Inputs get a label and a focus ring; buttons get a fill or a text colour. See [accessibility](/guide/accessibility/). --- ## 6.7 Control sizes | Size | Height | Use | |---|---|---| | `sm` | 32px | Table-row and toolbar controls | | `md` | 38px | Nav rows, inline buttons in page chrome | | `lg` | 48px | Form inputs and primary form buttons | **Hit area is 44px minimum**, including padding, even when the visible control is 32px. A 32px button in a table row needs vertical padding around it, not a smaller finger. --- ## 6.8 Grid and responsive Cards go **1 → 2 → 3** across `sm` (640) and `lg` (1024). Stat tiles go **1 → 2 → 4** across `sm` and `xl` (1280). | Breakpoint | Width | What changes | |---|---|---| | base | <640 | Single column. Sidebar is a drawer. Page gutter 24px | | `sm` | 640 | Two-column card grids | | `lg` | 1024 | Sidebar becomes static. Page gutter 32px | | `xl` | 1280 | Four-column stat grids; the page container caps | **Mobile is not an afterthought.** The primary audience reads on a phone in a truck. Every table must scroll inside its own box; every modal must fit a 375px viewport; every tap target must be 44px. --- ## 6.9 Do and don't **Do** - ✅ Land every dimension on the 4px scale - ✅ Separate with space first, a hairline second - ✅ Use warm-tinted shadows - ✅ Let a wide table scroll inside its own container **Don't** - ❌ Use `rgba(0,0,0,…)` shadows - ❌ Stack shadows to create depth — use one border - ❌ Invent a radius outside the scale - ❌ Go full-bleed on a product page - ❌ Ship a control below a 44px hit area --- # 07 · Iconography and imagery --- ## 7.1 The icon system **Line icons. 24×24 grid, 2px stroke, round caps and joins, no fill.** ```tsx
` with an `onClick`, it is a bug. --- ## 9.9 Operator and customer are different products The same codebase serves two audiences with different needs, and they get different vocabularies: | | Workspace (customer) | Operator console (staff) | |---|---|---| | Density | Comfortable | Dense | | Identifiers | Hidden | Visible, in mono | | Language | Plain — "Your apps" | Precise — "Entitlements" | | Errors | What to do next | What happened, with the ID | | Role shown | Membership role | Internal rank | Never leak operator vocabulary into the workspace. A customer should never see the word "entitlement", a UUID, or an internal role name. --- ## 9.10 The five-second test Export any screen or asset. Look at it for five seconds. Close it. Write down what you remember. If you cannot recall the main message, the hierarchy is wrong — start again. This applies to a dashboard as much as to a LinkedIn cover. --- # 10 · Patterns > Recurring problems and the Since Labs answer to each. Use the pattern; do not > re-solve it. --- ## 10.1 Page Every product page is the same three things. ```tsx
Run ingestion} /> {/* content */}
``` - **One `PageHeader` per page.** It owns the h1. Never hand-roll a title beside it. - Breadcrumbs whenever the page is more than one level deep. Last crumb is the current page and is not a link. - The description is one sentence, capped at 62ch, and says what the page is *for* — not that it exists. - Primary action top-right. One only. --- ## 10.2 Navigation **Platform sidebar:** four fixed items. **App sidebar:** takes over while you are inside an app, with an "All apps" escape hatch at the top. **Operator sidebar:** its own tree, and per-app surfaces hang off the Apps row. Active state is computed from the live pathname on the client — never from a request header. Active row: `accent-soft` background, `accent-ink` text, medium weight. Ancestor of the active row: `ink` text, no background. Children reveal only while their parent branch is active. --- ## 10.3 Command palette (⌘K) The app switcher, and the model for any future palette. - Opens on ⌘K / Ctrl-K from anywhere; `Esc` closes; the trigger is also clickable. - `role="dialog"`, `aria-modal="true"`, focus moves to the input on open. - `↑` `↓` wrap around; `↵` opens the highlighted row; hover moves the cursor. - The highlighted row scrolls into view — `scrollIntoView({ block: "nearest" })`. - Empty result says what was searched for: `No apps match "xyz".` - Footer shows the shortcuts and one escape link: "Browse all apps". - Unavailable rows render disabled at 45% opacity with a reason, not hidden. --- ## 10.4 Forms **Layout.** One column. Labels above fields, always visible. Fields at `lg` (48px). 16px between fields, 20px between groups. Primary action bottom-left of the form or full-width in a card. **Validation.** Validate on submit, not on every keystroke. Show the error under the field with `aria-describedby`, set `aria-invalid`, and colour the border `error-dot`. Summarise at the top only if the form is long enough to scroll. **Errors say what to do:** > ✅ "Enter a valid email address." > ❌ "Invalid input." **Pending.** Disable the submit and change the label — `Signing in…`, `Submitting…`. Never leave a button clickable through a submit. **Server actions.** A `